From 08ee8fd354d104eac1e6370bf0de878a69387455 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 30 Nov 2015 12:34:28 -0700 Subject: [PATCH 01/41] Working on filling code coverage gaps. --- .../api/acl_spec.rb} | 0 spec/system/rbeapi/api/bgp_neighbors_spec.rb | 354 +++++++++++++++++ spec/system/rbeapi/api/bgp_spec.rb | 375 ++++++++++++++++++ .../api/ospf_interfaces_spec.rb} | 0 .../api/ospf_spec.rb} | 0 spec/system/rbeapi/api/staticroutes_spec.rb | 177 +++++++++ 6 files changed, 906 insertions(+) rename spec/system/{api_acl_spec.rb => rbeapi/api/acl_spec.rb} (100%) create mode 100644 spec/system/rbeapi/api/bgp_neighbors_spec.rb create mode 100644 spec/system/rbeapi/api/bgp_spec.rb rename spec/system/{api_ospf_interfaces_spec.rb => rbeapi/api/ospf_interfaces_spec.rb} (100%) rename spec/system/{api_ospf_spec.rb => rbeapi/api/ospf_spec.rb} (100%) create mode 100644 spec/system/rbeapi/api/staticroutes_spec.rb diff --git a/spec/system/api_acl_spec.rb b/spec/system/rbeapi/api/acl_spec.rb similarity index 100% rename from spec/system/api_acl_spec.rb rename to spec/system/rbeapi/api/acl_spec.rb diff --git a/spec/system/rbeapi/api/bgp_neighbors_spec.rb b/spec/system/rbeapi/api/bgp_neighbors_spec.rb new file mode 100644 index 0000000..9a54027 --- /dev/null +++ b/spec/system/rbeapi/api/bgp_neighbors_spec.rb @@ -0,0 +1,354 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/bgp' + +describe Rbeapi::Api::BgpNeighbors do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + describe '#get' do + let(:entity) do + { peer_group: nil, + remote_as: nil, + send_community: false, + shutdown: false, + description: nil, + next_hop_self: false, + route_map_in: nil, + route_map_out: nil } + end + + before do + node.config(['no router bgp 64600', 'router bgp 64600', + 'neighbor eBGP_GROUP peer-group']) + end + + it 'returns the BGP neighbor resource' do + expect(subject.get('eBGP_GROUP')).to eq(entity) + end + end + + describe '#getall' do + let(:entity) do + { + 'eBGP_GROUP' => { + peer_group: nil, remote_as: nil, send_community: false, + shutdown: false, description: nil, next_hop_self: false, + route_map_in: nil, route_map_out: nil + }, + '192.168.255.1' => { + peer_group: 'eBGP_GROUP', remote_as: '65000', send_community: true, + shutdown: true, description: nil, next_hop_self: true, + route_map_in: nil, route_map_out: nil + }, + '192.168.255.3' => { + peer_group: 'eBGP_GROUP', remote_as: '65001', send_community: true, + shutdown: true, description: nil, next_hop_self: true, + route_map_in: nil, route_map_out: nil + } + } + end + + before do + node.config(['no router bgp 64600', 'router bgp 64600', + 'neighbor 192.168.255.1 peer-group eBGP_GROUP', + 'neighbor 192.168.255.1 remote-as 65000', + 'neighbor 192.168.255.3 peer-group eBGP_GROUP', + 'neighbor 192.168.255.3 remote-as 65001']) + end + + it 'returns all the neighbors' do + expect(subject.getall).to eq(entity) + end + + it 'returns a hash collection' do + expect(subject.getall).to be_a_kind_of(Hash) + end + + it 'has three entries' do + expect(subject.getall.size).to eq(3) + end + end + + describe '#create' do + let(:before) do + { peer_group: nil, + remote_as: nil, + send_community: true, + shutdown: true, + description: nil, + next_hop_self: true, + route_map_in: nil, + route_map_out: nil } + end + + let(:after) do + { peer_group: nil, + remote_as: nil, + send_community: false, + shutdown: true, + description: nil, + next_hop_self: false, + route_map_in: nil, + route_map_out: nil } + end + + before { node.config(['no router bgp 64600', 'router bgp 64600']) } + + it 'create a new BGP neighbor' do + expect(subject.get('edge')).to eq(before) + expect(subject.create('edge')).to be_truthy + + expect(subject.get('edge')).to eq(after) + end + end + + describe '#delete' do + let(:before) do + { peer_group: nil, + remote_as: nil, + send_community: false, + shutdown: true, + description: nil, + next_hop_self: false, + route_map_in: nil, + route_map_out: nil } + end + + let(:after) do + { peer_group: nil, + remote_as: nil, + send_community: true, + shutdown: true, + description: nil, + next_hop_self: true, + route_map_in: nil, + route_map_out: nil } + end + + it 'delete a BGP resource' do + expect(subject.get('edge')).to eq(before) + expect(subject.delete('edge')).to be_truthy + + expect(subject.get('edge')).to eq(after) + end + end + + describe '#set_peer_group' do + before do + node.config(['no router bgp 64600', 'router bgp 64600', + 'neighbor eBGP_GROUP peer-group']) + end + + it 'set the peer group' do + expect(subject.get('192.168.255.1')[:peer_group]).to eq(nil) + expect(subject.set_peer_group('192.168.255.1', value: 'eBGP_GROUP')) + .to be_truthy + expect(subject.get('192.168.255.1')[:peer_group]).to eq('eBGP_GROUP') + end + + it 'remove the peer group value' do + expect(subject.set_peer_group('192.168.255.1', value: 'eBGP_GROUP')) + .to be_truthy + expect(subject.get('192.168.255.1')[:peer_group]).to eq('eBGP_GROUP') + expect(subject.set_peer_group('192.168.255.1', enable: false)) + .to be_truthy + expect(subject.get('192.168.255.1')[:peer_group]).to eq(nil) + end + + it 'defaults the peer group value' do + expect(subject.set_peer_group('192.168.255.1', value: 'eBGP_GROUP')) + .to be_truthy + expect(subject.set_peer_group('192.168.255.1', default: true)) + .to be_truthy + expect(subject.get('192.168.255.1')[:peer_group]).to eq(nil) + end + end + + describe '#set_remote_as' do + it 'set the remote AS value' do + expect(subject.get('eng')[:remote_as]).to eq(nil) + expect(subject.set_remote_as('eng', value: '10')).to be_truthy + expect(subject.get('eng')[:remote_as]).to eq('10') + end + + it 'remove the remote AS value' do + expect(subject.get('eng')[:remote_as]).to eq('10') + expect(subject.set_remote_as('eng', enable: false)) + .to be_truthy + expect(subject.get('eng')[:remote_as]).to eq(nil) + end + + it 'defaults the remote AS value' do + expect(subject.set_remote_as('eng', value: '10')).to be_truthy + expect(subject.set_remote_as('eng', default: true)) + .to be_truthy + expect(subject.get('eng')[:remote_as]).to eq(nil) + end + end + + describe '#set_shutdown' do + it 'shutdown neighbor' do + expect(subject.get('eng')[:shutdown]).to eq(false) + expect(subject.set_shutdown('eng')).to be_truthy + expect(subject.get('eng')[:shutdown]).to eq(true) + end + + it 'negate shutdown neighbor' do + expect(subject.get('eng')[:shutdown]).to eq(true) + expect(subject.set_shutdown('eng', enable: false)).to be_truthy + expect(subject.get('eng')[:shutdown]).to eq(true) + end + + it 'default shutdown neighbor' do + expect(subject.get('eng')[:shutdown]).to eq(true) + expect(subject.set_shutdown('eng', default: true)).to be_truthy + expect(subject.get('eng')[:shutdown]).to eq(false) + end + end + + describe '#set_send_community' do + it 'enable neighbor send community' do + expect(subject.get('eng')[:send_community]).to eq(false) + expect(subject.set_send_community('eng')).to be_truthy + expect(subject.get('eng')[:send_community]).to eq(true) + end + + it 'negate neighbor send community' do + expect(subject.get('eng')[:send_community]).to eq(true) + expect(subject.set_send_community('eng', enable: false)).to be_truthy + expect(subject.get('eng')[:send_community]).to eq(false) + end + + it 'default neighbor send community' do + expect(subject.set_send_community('eng')).to be_truthy + expect(subject.get('eng')[:send_community]).to eq(true) + expect(subject.set_send_community('eng', default: true)).to be_truthy + expect(subject.get('eng')[:send_community]).to eq(false) + end + end + + describe '#set_next_hop_self' do + it 'enable neighbor next hop self' do + expect(subject.get('eng')[:next_hop_self]).to eq(false) + expect(subject.set_next_hop_self('eng')).to be_truthy + expect(subject.get('eng')[:next_hop_self]).to eq(true) + end + + it 'negate neighbor next hop self' do + expect(subject.get('eng')[:next_hop_self]).to eq(true) + expect(subject.set_next_hop_self('eng', enable: false)).to be_truthy + expect(subject.get('eng')[:next_hop_self]).to eq(false) + end + + it 'default neighbor next hop self' do + expect(subject.set_next_hop_self('eng')).to be_truthy + expect(subject.get('eng')[:next_hop_self]).to eq(true) + expect(subject.set_next_hop_self('eng', default: true)).to be_truthy + expect(subject.get('eng')[:next_hop_self]).to eq(false) + end + end + + describe '#set_route_map_in' do + it 'set route map in value' do + expect(subject.get('eng')[:route_map_in]).to eq(nil) + expect(subject.set_route_map_in('eng', value: 'edge')).to be_truthy + expect(subject.get('eng')[:route_map_in]).to eq('edge') + end + + it 'negate route map in value' do + expect(subject.get('eng')[:route_map_in]).to eq('edge') + expect(subject.set_route_map_in('eng', value: 'edge', enable: false)) + .to be_truthy + expect(subject.get('eng')[:route_map_in]).to eq(nil) + end + + it 'default route map in value' do + expect(subject.set_route_map_in('eng', value: 'edge')).to be_truthy + expect(subject.get('eng')[:route_map_in]).to eq('edge') + expect(subject.set_route_map_in('eng', value: 'edge', default: true)) + .to be_truthy + expect(subject.get('eng')[:route_map_in]).to eq(nil) + end + end + + describe '#set_route_map_out' do + it 'set route map out value' do + expect(subject.get('eng')[:route_map_out]).to eq(nil) + expect(subject.set_route_map_out('eng', value: 'edge')).to be_truthy + expect(subject.get('eng')[:route_map_out]).to eq('edge') + end + + it 'negate route map out value' do + expect(subject.get('eng')[:route_map_out]).to eq('edge') + expect(subject.set_route_map_out('eng', value: 'edge', enable: false)) + .to be_truthy + expect(subject.get('eng')[:route_map_out]).to eq(nil) + end + + it 'default route map out value' do + expect(subject.set_route_map_out('eng', value: 'edge')).to be_truthy + expect(subject.get('eng')[:route_map_out]).to eq('edge') + expect(subject.set_route_map_out('eng', value: 'edge', default: true)) + .to be_truthy + expect(subject.get('eng')[:route_map_out]).to eq(nil) + end + end + + describe '#set_description' do + it 'set the description value' do + expect(subject.get('eng')[:description]).to eq(nil) + expect(subject.set_description('eng', value: 'text')).to be_truthy + expect(subject.get('eng')[:description]).to eq('text') + end + + it 'negate the description value' do + expect(subject.get('eng')[:description]).to eq('text') + expect(subject.set_description('eng', enable: false)).to be_truthy + expect(subject.get('eng')[:description]).to eq(nil) + end + + it 'defaults the description value' do + expect(subject.set_description('eng', value: 'text')).to be_truthy + expect(subject.get('eng')[:description]).to eq('text') + expect(subject.set_description('eng', default: true)).to be_truthy + expect(subject.get('eng')[:description]).to eq(nil) + end + end +end diff --git a/spec/system/rbeapi/api/bgp_spec.rb b/spec/system/rbeapi/api/bgp_spec.rb new file mode 100644 index 0000000..063c11c --- /dev/null +++ b/spec/system/rbeapi/api/bgp_spec.rb @@ -0,0 +1,375 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/bgp' + +describe Rbeapi::Api::Bgp do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + let(:test) do + { bgp_as: '64600', + router_id: '192.168.254.1', + shutdown: false, + maximum_paths: 32, + maximum_ecmp_paths: 32, + networks: [ + { prefix: '192.168.254.1', masklen: 32, route_map: nil }, + { prefix: '192.168.254.2', masklen: 32, route_map: 'rmap' } + ], + neighbors: { + 'eBGP_GROUP' => { + peer_group: nil, remote_as: nil, send_community: false, + shutdown: false, description: nil, next_hop_self: false, + route_map_in: nil, route_map_out: nil + }, + '192.168.255.1' => { + peer_group: 'eBGP_GROUP', remote_as: '65000', send_community: true, + shutdown: true, description: nil, next_hop_self: true, + route_map_in: nil, route_map_out: nil + }, + '192.168.255.3' => { + peer_group: 'eBGP_GROUP', remote_as: '65001', send_community: true, + shutdown: true, description: nil, next_hop_self: true, + route_map_in: nil, route_map_out: nil + } + } + } + end + + describe '#get' do + before do + node.config(['no router bgp 64600', 'router bgp 64600', + 'no shutdown', + 'router-id 192.168.254.1', + 'bgp convergence time 300', + 'bgp convergence slow-peer time 90', + 'no bgp confederation identifier', + 'no update wait-for-convergence', + 'no update wait-install', + 'bgp log-neighbor-changes', + 'bgp default ipv4-unicast', + 'no bgp default ipv6-unicast', + 'timers bgp 60 180', + 'distance bgp 20 200 200', + 'graceful-restart stalepath-time 300', + 'no bgp cluster-id', + 'bgp client-to-client reflection', + 'graceful-restart-helper', + 'bgp peer-mac-resolution-timeout 0', + 'bgp enforce-first-as', + 'no default-metric', + 'no bgp always-compare-med', + 'no bgp bestpath med missing-as-worst', + 'no bgp bestpath med confed', + 'no bgp host-routes fib direct-install', + 'maximum-paths 32 ecmp 32', + 'bgp listen limit 1000', + 'no bgp aspath-cmp-include-nexthop', + 'no bgp tie-break-on-age', + 'no bgp tie-break-on-router-id', + 'no bgp tie-break-on-originator-id', + 'no bgp tie-break-on-cluster-list-length', + 'neighbor eBGP_GROUP peer-group', + 'no neighbor eBGP_GROUP remote-as', + 'no neighbor eBGP_GROUP import-localpref', + 'no neighbor eBGP_GROUP export-localpref', + 'no neighbor eBGP_GROUP description', + 'no neighbor eBGP_GROUP next-hop-self', + 'no neighbor eBGP_GROUP next-hop-peer', + 'no neighbor eBGP_GROUP allowas-in', + 'no neighbor eBGP_GROUP send-community', + 'no neighbor eBGP_GROUP shutdown', + 'no neighbor eBGP_GROUP remove-private-as', + 'no neighbor eBGP_GROUP out-delay', + 'no neighbor eBGP_GROUP local-as', + 'no neighbor eBGP_GROUP weight', + 'no neighbor eBGP_GROUP transport connection-mode passive', + 'no neighbor eBGP_GROUP update-source', + 'no neighbor eBGP_GROUP dont-capability-negotiate', + 'neighbor eBGP_GROUP fall-over bfd', + 'no neighbor eBGP_GROUP local-v6-addr', + 'neighbor eBGP_GROUP soft-reconfiguration inbound', + 'no neighbor eBGP_GROUP ebgp-multihop', + 'no neighbor eBGP_GROUP route-reflector-client', + 'no neighbor eBGP_GROUP timers', + 'no neighbor eBGP_GROUP route-map in', + 'neighbor eBGP_GROUP graceful-restart-helper', + 'no neighbor eBGP_GROUP route-map out', + 'no neighbor eBGP_GROUP prefix-list in', + 'no neighbor eBGP_GROUP prefix-list out', + 'neighbor eBGP_GROUP password 7 6C/HkrwJ53+dsHumQf49Wg==', + 'no neighbor eBGP_GROUP default-originate', + 'neighbor eBGP_GROUP enforce-first-as', + 'no neighbor eBGP_GROUP metric-out', + 'neighbor eBGP_GROUP maximum-routes 12000', + 'neighbor 192.168.255.1 peer-group eBGP_GROUP', + 'neighbor 192.168.255.1 remote-as 65000', + 'neighbor 192.168.255.3 peer-group eBGP_GROUP', + 'neighbor 192.168.255.3 remote-as 65001', + 'network 192.168.254.1/32', + 'network 192.168.254.2/32 route-map rmap', + 'bgp redistribute-internal', + 'aggregate-address 192.168.255.0/28 summary-only', + 'no redistribute connected', + 'no redistribute ospf match internal', + 'no redistribute ospf match external', + 'no redistribute ospf match nssa-external', + 'no redistribute ospf3 match internal', + 'no redistribute ospf3 match external', + 'no redistribute static', + 'no redistribute rip', + 'no redistribute aggregate', + 'address-family ipv4', + 'default neighbor eBGP_GROUP activate', + 'no neighbor eBGP_GROUP route-map in', + 'no neighbor eBGP_GROUP route-map out', + 'no neighbor eBGP_GROUP default-originate', + 'default neighbor 192.168.255.1 activate', + 'default neighbor 192.168.255.3 activate', + 'address-family ipv6', + 'default neighbor eBGP_GROUP activate', + 'no neighbor eBGP_GROUP route-map in', + 'no neighbor eBGP_GROUP route-map out', + 'no neighbor eBGP_GROUP prefix-list in', + 'no neighbor eBGP_GROUP prefix-list out', + 'no neighbor eBGP_GROUP default-originate', + 'default neighbor 192.168.255.1 activate', + 'default neighbor 192.168.255.3 activate']) + end + + it 'returns the BGP resource' do + expect(subject.get).to eq(test) + end + end + + describe '#create' do + let(:response) do + { + bgp_as: '1000', + router_id: nil, + shutdown: false, + maximum_paths: 1, + maximum_ecmp_paths: 128, + networks: [], + neighbors: {} + } + end + + before do + node.config(['no router bgp 64600']) + end + + it 'create a new BGP resource' do + expect(subject.get).to eq(nil) + expect(subject.create('1000')).to be_truthy + expect(subject.get).to eq(response) + end + + it 'create with enable' do + expect(subject.get).to eq(nil) + expect(subject.create('1000', enable: true)).to be_truthy + expect(subject.get).to eq(response) + end + + it 'create with router_id' do + expect(subject.get).to eq(nil) + expect(subject.create('1000', router_id: '1.2.3.4')).to be_truthy + expect(subject.get[:router_id]).to eq('1.2.3.4') + end + + it 'create with maximum paths' do + expect(subject.get).to eq(nil) + expect(subject.create('1000', maximum_paths: 3)).to be_truthy + expect(subject.get[:maximum_paths]).to eq(3) + end + + it 'create with maximum paths and ecmp paths' do + expect(subject.get).to eq(nil) + expect(subject.create('1000', maximum_paths: 13, + maximum_ecmp_paths: 13)).to be_truthy + expect(subject.get[:maximum_paths]).to eq(13) + expect(subject.get[:maximum_ecmp_paths]).to eq(13) + end + + it 'raises ArgumentError for create with ecmp paths only' do + expect { subject.create('1000', maximum_ecmp_paths: 13) }.to \ + raise_error ArgumentError + end + + it 'create with all options set' do + expect(subject.create('1000', enable: true, router_id: '1.2.3.4', + maximum_paths: 13, + maximum_ecmp_paths: 13)).to be_truthy + expect(subject.get[:router_id]).to eq('1.2.3.4') + expect(subject.get[:maximum_paths]).to eq(13) + expect(subject.get[:maximum_ecmp_paths]).to eq(13) + end + end + + describe '#delete' do + before do + node.config(['no router bgp 1000', 'router bgp 1000']) + end + + let(:response) do + { + bgp_as: '1000', + router_id: nil, + shutdown: false, + maximum_paths: 1, + maximum_ecmp_paths: 128, + networks: [], + neighbors: {} + } + end + + it 'delete a BGP resource' do + expect(subject.get).to eq(response) + expect(subject.delete).to be_truthy + expect(subject.get).to eq(nil) + end + end + + describe '#default' do + before do + node.config(['no router bgp 1000', 'router bgp 1000', + 'maximum-paths 13 ecmp 15']) + end + + let(:before) do + { + bgp_as: '1000', + router_id: nil, + shutdown: false, + maximum_paths: 13, + maximum_ecmp_paths: 15, + networks: [], + neighbors: {} + } + end + + it 'sets router to default value' do + expect(subject.get).to eq(before) + expect(subject.default).to be_truthy + expect(subject.get).to eq(nil) + end + end + + describe '#set_router_id' do + before do + node.config(['no router bgp 1000', 'router bgp 1000']) + end + + it 'set the router id' do + expect(subject.set_router_id(value: '1.2.3.4')).to be_truthy + expect(subject.get[:router_id]).to eq('1.2.3.4') + end + + it 'remove the router-id without a value' do + expect(subject.set_router_id(enable: false)).to be_truthy + expect(subject.get[:router_id]).to eq(nil) + end + + it 'remove the router-id with a value' do + expect(subject.set_router_id(value: '1.2.3.4', enable: false)) + .to be_truthy + expect(subject.get[:router_id]).to eq(nil) + end + + it 'defaults the router-id without a value' do + expect(subject.set_router_id(value: '1.2.3.4')).to be_truthy + expect(subject.set_router_id(default: true)).to be_truthy + expect(subject.get[:router_id]).to eq(nil) + end + + it 'defaults the router-id with a value' do + expect(subject.set_router_id(value: '1.2.3.4', default: true)) + .to be_truthy + expect(subject.get[:router_id]).to eq(nil) + end + end + + describe '#set_shutdown' do + it 'enable BGP routing process' do + expect(subject.set_shutdown(enable: true)).to be_truthy + end + + it 'disable BGP routing process' do + expect(subject.set_shutdown(enable: false)).to be_truthy + end + + it 'default BGP routing process state' do + expect(subject.set_shutdown(default: true)).to be_truthy + end + end + + describe '#set_maximum_paths' do + it 'set the maximum paths and ecmp paths' do + expect(subject.set_maximum_paths(13, 16)).to be_truthy + end + + it 'remove the maximum paths' do + expect(subject.set_maximum_paths(0, 0, enable: false)).to be_truthy + end + + it 'defaults the maximum paths' do + expect(subject.set_maximum_paths(0, 0, default: true)).to be_truthy + end + end + + describe '#add_network' do + it 'add a BGP network with a route map' do + expect(subject.add_network('1.2.3.0', 24, 'eng')).to be_truthy + end + + it 'add a BGP network without a route map' do + expect(subject.add_network('1.2.3.0', 24)).to be_truthy + end + end + + describe '#remove_network' do + it 'remove a BGP network with a route map' do + expect(subject.remove_network('1.2.3.0', 24, 'eng')).to be_truthy + end + + it 'remove a BGP network without a route map' do + expect(subject.remove_network('1.2.3.0', 24)).to be_truthy + end + end +end diff --git a/spec/system/api_ospf_interfaces_spec.rb b/spec/system/rbeapi/api/ospf_interfaces_spec.rb similarity index 100% rename from spec/system/api_ospf_interfaces_spec.rb rename to spec/system/rbeapi/api/ospf_interfaces_spec.rb diff --git a/spec/system/api_ospf_spec.rb b/spec/system/rbeapi/api/ospf_spec.rb similarity index 100% rename from spec/system/api_ospf_spec.rb rename to spec/system/rbeapi/api/ospf_spec.rb diff --git a/spec/system/rbeapi/api/staticroutes_spec.rb b/spec/system/rbeapi/api/staticroutes_spec.rb new file mode 100644 index 0000000..a4980e2 --- /dev/null +++ b/spec/system/rbeapi/api/staticroutes_spec.rb @@ -0,0 +1,177 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/staticroutes' + +describe Rbeapi::Api::Staticroutes do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + describe '#getall' do + let(:resource) { subject.getall } + + before do + node.config(['no ip route 1.2.3.4/32', + 'no ip route 192.0.2.0/24', + 'no ip route 192.0.3.0/24', + 'ip route 1.2.3.4/32 Ethernet7 4 tag 3 name frank', + 'ip route 1.2.3.4/32 Null0 32 tag 3 name fred', + 'ip route 192.0.2.0/24 Ethernet7 3 tag 0 name dummy1', + 'ip route 192.0.3.0/24 192.0.3.1 1 tag 0 name dummy2']) + end + + it 'returns the staticroute collection' do + expect(subject.getall).to include(destination: '1.2.3.4/32', + nexthop: 'Ethernet7', + distance: '4', + tag: '3', + name: 'frank') + expect(subject.getall).to include(destination: '1.2.3.4/32', + nexthop: 'Null0', + distance: '32', + tag: '3', + name: 'fred') + expect(subject.getall).to include(destination: '192.0.2.0/24', + nexthop: 'Ethernet7', + distance: '3', + tag: '0', + name: 'dummy1') + expect(subject.getall).to include(destination: '192.0.3.0/24', + nexthop: '192.0.3.1', + distance: '1', + tag: '0', + name: 'dummy2') + end + + it 'returns a hash collection' do + expect(subject.getall).to be_a_kind_of(Array) + end + + it 'has four entries' do + expect(subject.getall.size).to eq(4) + end + end + + describe '#create' do + let(:resource) { subject.getall } + + before(:each) do + node.config(['no ip route 1.2.3.4/32', + 'no ip route 192.0.2.0/24', + 'no ip route 192.0.3.0/24']) + end + + context 'creates a new staticroute resoure' do + it 'with minimum input' do + expect(subject.getall).to eq([]) + expect(subject.create('192.0.2.0/24', 'Ethernet1')).to be_truthy + expect(subject.getall).to eq(resource) + end + + it 'with a router_ip' do + node.config(['ip route 192.0.2.0/24 Ethernet1']) + expect(subject.getall).to eq(resource) + + expect(subject.create('192.0.2.0/24', 'Ethernet1', + router_ip: '192.168.1.1')).to be_truthy + expect(subject.getall).to eq(resource) + end + + it 'with distance (metric)' do + node.config(['ip route 192.0.2.0/24 Ethernet1']) + expect(subject.getall).to eq(resource) + + expect(subject.create('192.0.2.0/24', 'Ethernet1', distance: 254)) + .to be_truthy + expect(subject.getall).to include(destination: '192.0.2.0/24', + nexthop: 'Ethernet1', + distance: '254', + tag: '0', + name: nil) + end + + it 'with a tag' do + node.config(['ip route 192.0.2.0/24 Ethernet1']) + expect(subject.getall).to eq(resource) + + expect(subject.create('192.0.2.0/24', 'Ethernet1', tag: 3)) + .to be_truthy + expect(subject.getall).to include(destination: '192.0.2.0/24', + nexthop: 'Ethernet1', + distance: '1', + tag: '3', + name: nil) + end + + it 'with a name' do + node.config(['ip route 192.0.2.0/24 Ethernet1']) + expect(subject.getall).to eq(resource) + + expect(subject.create('192.0.2.0/24', 'Ethernet1', name: 'my_route')) + .to be_truthy + expect(subject.getall).to include(destination: '192.0.2.0/24', + nexthop: 'Ethernet1', + distance: '1', + tag: '0', + name: 'my_route') + end + end + end + + describe '#delete' do + let(:resource) { subject.getall } + + before do + node.config(['ip route 192.0.2.0/24 Ethernet1']) + end + + context 'deletes a staticroute resource' do + it 'given only a destination network' do + expect(subject.getall).to eq(resource) + expect(subject.delete('192.0.2.0/24')).to be_truthy + expect(subject.getall).to eq([]) + end + + it 'given a destination and nexthop' do + expect(subject.getall).to eq(resource) + expect(subject.delete('192.0.2.0/24', 'Ethernet1')).to be_truthy + expect(subject.getall).to eq([]) + end + end + end +end From e26b9743216e8cfe9867c11b9e5663c2b498be47 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 30 Nov 2015 16:26:37 -0700 Subject: [PATCH 02/41] Ensure that nil is returned when you try to get nonexistent username. --- lib/rbeapi/api/users.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rbeapi/api/users.rb b/lib/rbeapi/api/users.rb index c546f9a..6d5cde9 100644 --- a/lib/rbeapi/api/users.rb +++ b/lib/rbeapi/api/users.rb @@ -94,7 +94,7 @@ def get(name) (username\s+#{name}\s+ sshkey\s+(?.*)$)?/x) user = config.scan(user_re) - return nil unless user + return nil unless user && user[0] parse_user_entry(user[0]) end From cdd3dc06575b055521708b07c1b7fcff62aba5da Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Tue, 1 Dec 2015 14:03:16 -0700 Subject: [PATCH 03/41] Adding users system test. Currently having an issue with sha512 encryption. --- spec/system/rbeapi/api/users_spec.rb | 322 +++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 spec/system/rbeapi/api/users_spec.rb diff --git a/spec/system/rbeapi/api/users_spec.rb b/spec/system/rbeapi/api/users_spec.rb new file mode 100644 index 0000000..d6d8a1a --- /dev/null +++ b/spec/system/rbeapi/api/users_spec.rb @@ -0,0 +1,322 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/users' + +describe Rbeapi::Api::Users do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + let(:sshkey) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKL1UtBALa4CvFUsHUipN' \ + 'ymA04qCXuAtTwNcMj84bTUzUI+q7mdzRCTLkllXeVxKuBnaTm2PW7W67K5C' \ + 'Vpl0EVCm6IY7FS7kc4nlnD/tFvTvShy/fzYQRAdM7ZfVtegW8sMSFJzBR/T' \ + '/Y/sxI16Y/dQb8fC3la9T25XOrzsFrQiKRZmJGwg8d+0RLxpfMg0s/9ATwQ' \ + 'Kp6tPoLE4f3dKlAgSk5eENyVLA3RsypWADHpenHPcB7sa8D38e1TS+n+EUy' \ + 'Adb3Yov+5ESAbgLIJLd52Xv+FyYi0c2L49ByBjcRrupp4zfXn4DNRnEG4K6' \ + 'GcmswHuMEGZv5vjJ9OYaaaaaaa' + end + + let(:secret) do + '$1$Ehb5lL0D$N3MgrkfMFxmeh0FSZ5sEZ1' + end + + let(:test) do + { name: 'rbeapi', + privilege: 1, + role: nil, + nopassword: false, + encryption: 'md5', + secret: secret, + sshkey: sshkey + } + end + + describe '#getall' do + let(:resource) { subject.getall } + + let(:test1_entries) do + { 'admin' => { name: 'admin', privilege: 1, + role: 'network-admin', nopassword: true, + encryption: nil, secret: nil, sshkey: nil }, + 'rbeapi' => { name: 'rbeapi', privilege: 1, role: nil, + nopassword: false, encryption: 'md5', + secret: secret, + sshkey: sshkey } + } + end + + before do + node.config(['no username rbeapi', + 'no username user1', + 'username admin privilege 1 role network-admin nopassword', + "username rbeapi privilege 1 secret 5 #{secret}", + "username rbeapi sshkey #{sshkey}"]) + end + + it 'returns the username collection' do + expect(subject.getall).to include(test1_entries) + end + + it 'returns a hash collection' do + expect(subject.getall).to be_a_kind_of(Hash) + end + + it 'has two entries' do + expect(subject.getall.size).to eq(2) + end + end + + describe '#get' do + it 'returns the user resource for given name' do + expect(subject.get('rbeapi')).to eq(test) + end + + it 'returns a hash' do + expect(subject.get('rbeapi')).to be_a_kind_of(Hash) + end + + it 'has two entries' do + expect(subject.get('rbeapi').size).to eq(7) + end + end + + describe '#create' do + before do + node.config(['no username rbeapi']) + end + + it 'create a new user name with no password' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', nopassword: :true)).to be_truthy + expect(subject.get('rbeapi')[:nopassword]).to eq(true) + end + + it 'create a new user name with no password and privilege' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + privilege: 4, + nopassword: :true)).to be_truthy + expect(subject.get('rbeapi')[:privilege]).to eq(4) + end + + it 'create a new user name with no password, privilege, and role' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + privilege: 4, + role: 'net-minion', + nopassword: :true)).to be_truthy + expect(subject.get('rbeapi')[:privilege]).to eq(4) + expect(subject.get('rbeapi')[:role]).to eq('net-minion') + expect(subject.get('rbeapi')[:nopassword]).to eq(true) + end + + it 'create a new user name with a password' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', secret: 'icanttellyou')).to be_truthy + expect(subject.get('rbeapi')[:encryption]).to eq('md5') + end + + it 'create a new user name with a password and privilege' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + secret: 'icanttellyou', + privilege: 5)).to be_truthy + expect(subject.get('rbeapi')[:encryption]).to eq('md5') + expect(subject.get('rbeapi')[:privilege]).to eq(5) + end + + it 'create a new user name with a password, privilege, and role' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + secret: 'icanttellyou', + privilege: 5, role: 'net')).to be_truthy + expect(subject.get('rbeapi')[:encryption]).to eq('md5') + expect(subject.get('rbeapi')[:privilege]).to eq(5) + expect(subject.get('rbeapi')[:role]).to eq('net') + end + + it 'create a new user name with a password and md5 encryption' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + secret: '$1$Wb4zN5EH$ILNgYb3Ehzs85S9KpoFW4.', + encryption: 'md5')).to be_truthy + expect(subject.get('rbeapi')[:encryption]).to eq('md5') + expect(subject.get('rbeapi')[:secret]) + .to eq('$1$Wb4zN5EH$ILNgYb3Ehzs85S9KpoFW4.') + end + + it 'create a new user name with a password and sha512 encryption' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + secret: '$6$somesalt$rkDq7Az4Efjo', + encryption: 'sha512')).to be_truthy + expect(subject.get('rbeapi')).to eq(nil) + end + + it 'create a new user name with a password, sha512 encryption, and key' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + secret: '$6$somesalt$rkDq7Az4Efjo', + encryption: 'sha512', + sshkey: sshkey)).to be_truthy + end + + it 'raises ArgumentError for create without required args ' do + expect { subject.create('rbeapi') }.to \ + raise_error ArgumentError + end + + it 'raises ArgumentError for invalid encryption value' do + expect { subject.create('name', encryption: 'bogus') }.to \ + raise_error ArgumentError + end + end + + describe '#delete' do + before do + node.config(['username user1 privilege 1 role network-admin nopassword']) + end + + it 'delete a username resource' do + expect(subject.get('user1')[:name]).to eq('user1') + expect(subject.delete('user1')).to be_truthy + expect(subject.get('user1')).to eq(nil) + end + end + + describe '#default' do + before do + node.config(['username user1 privilege 1 role network-admin nopassword']) + end + + it 'sets username resource to default value' do + expect(subject.get('user1')[:name]).to eq('user1') + expect(subject.default('user1')).to be_truthy + expect(subject.get('user1')).to eq(nil) + end + end + + describe '#set_privilege' do + before do + node.config(['no username rbeapi', + 'username rbeapi role network-admin nopassword']) + end + + it 'set the privilege' do + expect(subject.set_privilege('rbeapi', value: '13')).to be_truthy + expect(subject.get('rbeapi')[:privilege]).to eq(13) + end + + it 'remove the privilege without a value' do + expect(subject.set_privilege('rbeapi', enable: false)).to be_truthy + expect(subject.get('rbeapi')).to eq(nil) + end + + it 'remove the privilege with a value' do + expect(subject.set_privilege('rbeapi', value: '13', enable: false)) + .to be_truthy + expect(subject.get('rbeapi')).to eq(nil) + end + + it 'defaults the privilege without a value' do + expect(subject.set_privilege('rbeapi', default: true)).to be_truthy + expect(subject.get('rbeapi')).to eq(nil) + end + + it 'defaults the privilege with a value' do + expect(subject.set_privilege('rbeapi', value: '3', default: true)) + .to be_truthy + expect(subject.get('rbeapi')).to eq(nil) + end + end + + describe '#set_role' do + before do + node.config(['no username rbeapi', 'username rbeapi nopassword']) + end + + it 'set the role' do + expect(subject.set_role('rbeapi', value: 'net-minion')).to be_truthy + expect(subject.get('rbeapi')[:role]).to eq('net-minion') + end + + it 'remove the role without a value' do + expect(subject.set_role('rbeapi', enable: false)).to be_truthy + expect(subject.get('rbeapi')[:role]).to eq(nil) + end + + it 'remove the role with a value' do + expect(subject.set_role('rbeapi', value: 'net', enable: false)) + .to be_truthy + expect(subject.get('rbeapi')[:role]).to eq(nil) + end + + it 'defaults the role without a value' do + expect(subject.set_role('rbeapi', default: true)).to be_truthy + expect(subject.get('rbeapi')[:role]).to eq(nil) + end + + it 'defaults the role with a value' do + expect(subject.set_role('rbeapi', value: 'net', default: true)) + .to be_truthy + expect(subject.get('rbeapi')[:role]).to eq(nil) + end + end + + describe '#set_sshkey' do + before do + node.config(['no username rbeapi', 'username rbeapi nopassword']) + end + + it 'set the sshkey' do + expect(subject.set_sshkey('rbeapi', value: sshkey)).to be_truthy + end + + it 'remove the sshkey with a value' do + expect(subject.set_sshkey('rbeapi', value: sshkey, enable: false)) + .to be_truthy + expect(subject.get('rbeapi')[:sshkey]).to eq(nil) + end + + it 'defaults the sshkey without a value' do + expect(subject.set_sshkey('rbeapi', default: true)).to be_truthy + expect(subject.get('rbeapi')[:sshkey]).to eq(nil) + end + end +end From 648851f3f5828015ea7517b9d632e6b68bc9b6c3 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Wed, 2 Dec 2015 19:33:48 -0700 Subject: [PATCH 04/41] Add vrrp system test --- spec/system/rbeapi/api/vrrp_spec.rb | 703 ++++++++++++++++++++++++++++ 1 file changed, 703 insertions(+) create mode 100644 spec/system/rbeapi/api/vrrp_spec.rb diff --git a/spec/system/rbeapi/api/vrrp_spec.rb b/spec/system/rbeapi/api/vrrp_spec.rb new file mode 100644 index 0000000..55f2ddc --- /dev/null +++ b/spec/system/rbeapi/api/vrrp_spec.rb @@ -0,0 +1,703 @@ +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/vrrp' + +describe Rbeapi::Api::Vrrp do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + before :all do + @sec_ips = ['1.2.3.1', '1.2.3.2', '1.2.3.3', '1.2.3.4'] + @tracks = [{ name: 'Ethernet3', action: 'decrement', amount: 33 }, + { name: 'Ethernet2', action: 'decrement', amount: 22 }, + { name: 'Ethernet2', action: 'shutdown' }] + end + + describe '#get' do + before do + node.config(['no interface Vlan150', 'no interface Vlan100', + 'interface Vlan100', 'interface Vlan150', + 'ip address 40.10.5.8/24', 'vrrp 30 priority 100', + 'vrrp 30 timers advertise 1', + 'vrrp 30 mac-address advertisement-interval 30', + 'no vrrp 30 preempt', 'vrrp 30 preempt delay minimum 0', + 'vrrp 30 preempt delay reload 0', 'vrrp 30 delay reload 0', + 'no vrrp 30 authentication', 'vrrp 30 ip 40.10.5.31', + 'vrrp 30 ipv6 ::', 'vrrp 30 description The description', + 'vrrp 30 shutdown', 'vrrp 30 track Ethernet1 decrement 5', + 'no vrrp 30 bfd ip', 'no vrrp 30 bfd ipv6', + 'vrrp 30 ip version 2', 'vrrp 40 priority 200', + 'vrrp 40 timers advertise 1', + 'vrrp 40 mac-address advertisement-interval 30', + 'vrrp 40 preempt', 'vrrp 40 preempt delay minimum 0', + 'vrrp 40 preempt delay reload 0', 'vrrp 40 delay reload 0', + 'no vrrp 40 authentication', 'vrrp 40 ip 40.10.5.32', + 'vrrp 40 ipv6 ::', 'no vrrp 40 description', + 'no vrrp 40 shutdown', + 'vrrp 40 track Ethernet3 decrement 33', + 'vrrp 40 track Ethernet2 decrement 22', + 'vrrp 40 track Ethernet2 shutdown', 'no vrrp 40 bfd ip', + 'no vrrp 40 bfd ipv6', 'vrrp 40 ip version 2']) + end + + let(:entity) do + { 30 => { primary_ip: '40.10.5.31', delay_reload: 0, + description: 'The description', enable: false, ip_version: 2, + mac_addr_adv_interval: 30, preempt: false, preempt_delay_min: 0, + preempt_delay_reload: 0, priority: 100, secondary_ip: [], + timers_advertise: 1, + track: [ + { name: 'Ethernet1', action: 'decrement', amount: 5 } + ] + }, + 40 => { primary_ip: '40.10.5.32', delay_reload: 0, description: nil, + enable: true, ip_version: 2, mac_addr_adv_interval: 30, + preempt: true, preempt_delay_min: 0, preempt_delay_reload: 0, + priority: 200, secondary_ip: [], timers_advertise: 1, + track: @tracks + } + } + end + + it 'returns the virtual router resource' do + expect(subject.get('Vlan150')).to eq(entity) + end + end + + describe '#getall' do + it 'returns a hash collection' do + expect(subject.getall).to be_a_kind_of(Hash) + end + + it 'returns the virtual router collection' do + expect(subject.getall).to include('Vlan100') + expect(subject.getall).to include('Vlan150') + end + end + + describe '#create' do + before do + node.config(['no interface Vlan100']) + end + + it 'creates a new virtual router with enable true' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, enable: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'creates a new virtual router with enable false' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')).to include(9) + end + + it 'creates a new virtual router with primary ip' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '1.2.3.4')).to be_truthy + expect(subject.get('Vlan100')[9][:primary_ip]).to eq('1.2.3.4') + end + + it 'creates a new virtual router with priority' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '1.2.3.4', + priority: 100)).to be_truthy + expect(subject.get('Vlan100')[9][:priority]).to eq(100) + end + + it 'creates a new virtual router with description' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, description: 'Desc')).to be_truthy + expect(subject.get('Vlan100')[9][:description]).to eq('Desc') + end + + it 'creates a new virtual router with secondary ips' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', 9, + secondary_ip: ['100.99.98.71', + '100.99.98.70'])).to be_truthy + expect(subject.get('Vlan100')[9][:secondary_ip]).to eq(['100.99.98.70', + '100.99.98.71']) + end + + it 'creates a new virtual router with ip version 2' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', + 9, + primary_ip: '100.99.98.100', + ip_version: 2)).to be_truthy + expect(subject.get('Vlan100')[9][:ip_version]).to eq(2) + end + + it 'creates a new virtual router with timers advertise' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, timers_advertise: 77)).to be_truthy + expect(subject.get('Vlan100')[9][:timers_advertise]).to eq(77) + end + + it 'creates a new virtual router with mac addr adv interval' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, mac_addr_adv_interval: 77)) + .to be_truthy + expect(subject.get('Vlan100')[9][:mac_addr_adv_interval]).to eq(77) + end + + it 'creates a new virtual router with preemt true' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', 9, preempt: true)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(true) + end + + it 'creates a new virtual router with preemt false' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, preempt: false)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(false) + end + + it 'creates a new virtual router with preempt delay min' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, preempt_delay_min: 100)) + .to be_truthy + expect(subject.get('Vlan100')[9][:preempt_delay_min]).to eq(100) + end + + it 'creates a new virtual router with preempt delay reload' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, preempt_delay_reload: 100)) + .to be_truthy + expect(subject.get('Vlan100')[9][:preempt_delay_reload]).to eq(100) + end + + it 'creates a new virtual router with preempt delay reload' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, delay_reload: 100)).to be_truthy + expect(subject.get('Vlan100')[9][:delay_reload]).to eq(100) + end + + it 'creates a new virtual router with track values' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', + 9, + track: [{ name: 'Ethernet3', + action: 'decrement', + amount: 33 }, + { name: 'Ethernet2', + action: 'decrement', + amount: 22 }, + { name: 'Ethernet2', + action: 'shutdown' }])).to be_truthy + expect(subject.get('Vlan100')[9][:track]).to eq(@tracks) + end + + it 'creates a new virtual router resource with enable and primary ip' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, enable: true, primary_ip: '1.2.3.4')) + .to be_truthy + expect(subject.get('Vlan100')[9][:primary_ip]).to eq('1.2.3.4') + end + + it 'creates a new virtual router resource with enable and priority' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', 9, enable: true, priority: 100)) + .to be_truthy + expect(subject.get('Vlan100')[9][:priority]).to eq(100) + end + + it 'creates a new virtual router resource with enable and description' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, enable: true, description: 'Desc')) + .to be_truthy + expect(subject.get('Vlan100')[9][:description]).to eq('Desc') + end + + it 'creates a new virtual router resource with enable and secondary_ip' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', + 9, + enable: true, + secondary_ip: ['1.2.3.1', + '1.2.3.2', + '1.2.3.3', '1.2.3.4'])).to be_truthy + expect(subject.get('Vlan100')[9][:secondary_ip]).to eq(@sec_ips) + end + + it 'creates a new virtual router resource with all options set' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', + 9, + enable: true, + priority: 100, + description: 'Desc', + secondary_ip: ['100.99.98.71', + '100.99.98.70'], + ip_version: 2, + timers_advertise: 77, + mac_addr_adv_interval: 77, + preempt: true, + preempt_delay_min: 100, + preempt_delay_reload: 100, + delay_reload: 100, + track: [{ name: 'Ethernet3', + action: 'decrement', + amount: 33 }, + { name: 'Ethernet2', + action: 'decrement', + amount: 22 }, + { name: 'Ethernet2', + action: 'shutdown' }])).to be_truthy + end + + it 'raises ArgumentError for create without options' do + expect { subject.create('Vlan100', 9) }.to \ + raise_error ArgumentError + end + end + + describe '#delete' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'deletes a virtual router resource' do + expect(subject.delete('Vlan100', 9)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#default' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'sets virtual router resource to default' do + expect(subject.default('Vlan100', 9)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_shutdown' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'enable Vlan100 vrid 9' do + expect(subject.set_shutdown('Vlan100', 9)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'disable Vlan100 vrid 9' do + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.set_shutdown('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')[9][:enable]).to eq(false) + end + + it 'defaults Vlan100 vrid 9' do + expect(subject.set_shutdown('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_shutdown('Vlan100', 9, enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_primary_ip' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set primary IP address' do + expect(subject.set_primary_ip('Vlan100', 9, + value: '1.2.3.4')).to be_truthy + expect(subject.get('Vlan100')[9][:primary_ip]).to eq('1.2.3.4') + end + + it 'disable primary IP address' do + expect(subject.set_primary_ip('Vlan100', 9, value: '1.2.3.4', + enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults primary IP address' do + expect(subject.set_primary_ip('Vlan100', 9, value: '1.2.3.4', + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_primary_ip('Vlan100', 9, enable: false, + value: '1.2.3.4', + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_priority' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set priority' do + expect(subject.set_priority('Vlan100', 9, value: 13)).to be_truthy + expect(subject.get('Vlan100')[9][:priority]).to eq(13) + end + + it 'disable priority' do + expect(subject.set_priority('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults priority' do + expect(subject.set_priority('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_priority('Vlan100', 9, enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_description' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set description' do + expect(subject.set_description('Vlan100', 9, + value: 'Howdy')).to be_truthy + expect(subject.get('Vlan100')[9][:description]).to eq('Howdy') + end + + it 'disable description' do + expect(subject.set_description('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults description' do + expect(subject.set_description('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_description('Vlan100', 9, enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_secondary_ip' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set secondary IP addresses' do + # Set current IP addresses + expect(subject.set_secondary_ip('Vlan100', 9, @sec_ips)).to be_truthy + expect(subject.get('Vlan100')[9][:secondary_ip]).to eq(@sec_ips) + end + + it 'remove all secondary IP addresses' do + # Set current IP addresses + expect(subject.set_secondary_ip('Vlan100', 9, @sec_ips)).to be_truthy + # Delete all IP addresses + expect(subject.set_secondary_ip('Vlan100', 9, [])).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_ip_version' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set VRRP version' do + expect(subject.set_ip_version('Vlan100', 9, value: 3)).to be_truthy + expect(subject.get('Vlan100')[9][:ip_version]).to eq(3) + end + + it 'disable VRRP version' do + expect(subject.set_ip_version('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults VRRP version' do + expect(subject.set_ip_version('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_ip_version('Vlan100', 9, enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_timers_advertise' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set advertise timer' do + expect(subject.set_timers_advertise('Vlan100', 9, value: 7)).to be_truthy + expect(subject.get('Vlan100')[9][:timers_advertise]).to eq(7) + end + + it 'disable advertise timer' do + expect(subject.set_timers_advertise('Vlan100', 9, + enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults advertise timer' do + expect(subject.set_timers_advertise('Vlan100', 9, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_timers_advertise('Vlan100', 9, + enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_mac_addr_adv_interval' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set mac address advertisement interval' do + expect(subject.set_mac_addr_adv_interval('Vlan100', 9, + value: 12)).to be_truthy + expect(subject.get('Vlan100')[9][:mac_addr_adv_interval]).to eq(12) + end + + it 'disable mac address advertisement interval' do + expect(subject.set_mac_addr_adv_interval('Vlan100', 9, + enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults mac address advertisement interval' do + expect(subject.set_mac_addr_adv_interval('Vlan100', 9, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_mac_addr_adv_interval('Vlan100', 9, + enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_preempt' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'enable preempt mode' do + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.set_preempt('Vlan100', 9)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(true) + end + + it 'disable preempt mode' do + expect(subject.set_preempt('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(false) + end + + it 'defaults preempt mode' do + expect(subject.set_preempt('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(false) + end + + it 'default option takes precedence' do + expect(subject.set_preempt('Vlan100', 9, enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(false) + end + end + + describe '#set_preempt_delay_min' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'enable preempt mode' do + expect(subject.set_preempt_delay_min('Vlan100', 9, value: 8)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt_delay_min]).to eq(8) + end + + it 'disable preempt mode' do + expect(subject.set_preempt_delay_min('Vlan100', 9, + enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults preempt mode' do + expect(subject.set_preempt_delay_min('Vlan100', 9, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_preempt_delay_min('Vlan100', 9, + enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_preempt_delay_reload' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'enable preempt delay reload' do + expect(subject.set_preempt_delay_reload('Vlan100', 9, + value: 8)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt_delay_reload]).to eq(8) + end + + it 'disable preempt delay reload' do + expect(subject.set_preempt_delay_reload('Vlan100', 9, + enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults preempt delay reload' do + expect(subject.set_preempt_delay_reload('Vlan100', 9, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_preempt_delay_reload('Vlan100', 9, + enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_delay_reload' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'enable delay reload' do + expect(subject.set_delay_reload('Vlan100', 9, value: 8)).to be_truthy + expect(subject.get('Vlan100')[9][:delay_reload]).to eq(8) + end + + it 'disable delay reload' do + expect(subject.set_delay_reload('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults delay reload' do + expect(subject.set_delay_reload('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_delay_reload('Vlan100', 9, + enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_tracks' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + before :all do + @bad_key = [{ nombre: 'Ethernet3', action: 'decrement', amount: 33 }] + @miss_key = [{ action: 'decrement', amount: 33 }] + @bad_action = [{ name: 'Ethernet3', action: 'dec', amount: 33 }] + @sem_key = [{ name: 'Ethernet3', action: 'shutdown', amount: 33 }] + @bad_amount = [{ name: 'Ethernet3', action: 'decrement', amount: -1 }] + end + + it 'set tracks' do + # Set current IP addresses + expect(subject.set_tracks('Vlan100', 9, @tracks)).to be_truthy + expect(subject.get('Vlan100')[9][:track]).to eq(@tracks) + end + + it 'remove all tracks' do + # Set current IP addresses + expect(subject.set_tracks('Vlan100', 9, @tracks)).to be_truthy + # Delete all IP addresses + expect(subject.set_tracks('Vlan100', 9, [])).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'raises ArgumentError for track hash with a bad key' do + expect { subject.set_tracks('Vlan100', 9, @bad_key) }.to \ + raise_error ArgumentError + end + + it 'raises ArgumentError for track hash with missing required key' do + expect { subject.set_tracks('Vlan100', 9, @miss_key) }.to \ + raise_error ArgumentError + end + + it 'raises ArgumentError for track hash with invalid action' do + expect { subject.set_tracks('Vlan100', 9, @bad_action) }.to \ + raise_error ArgumentError + end + + it 'raises ArgumentError for track hash with shutdown and amount' do + expect { subject.set_tracks('Vlan100', 9, @sem_key) }.to \ + raise_error ArgumentError + end + + it 'raises ArgumentError for track hash with negative amount' do + expect { subject.set_tracks('Vlan100', 9, @bad_amount) }.to \ + raise_error ArgumentError + end + end +end From e8251c7959c0f49d1aafec0f5e3ee775c9470d34 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Wed, 2 Dec 2015 20:16:44 -0700 Subject: [PATCH 05/41] Add valid sha512 password to user system test --- spec/system/rbeapi/api/users_spec.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/spec/system/rbeapi/api/users_spec.rb b/spec/system/rbeapi/api/users_spec.rb index d6d8a1a..57925ca 100644 --- a/spec/system/rbeapi/api/users_spec.rb +++ b/spec/system/rbeapi/api/users_spec.rb @@ -53,6 +53,11 @@ end let(:secret) do + '$6$RMxgK5ALGIf.nWEC$tHuKCyfNtJMCY561P52dTzHUmYMmLxb/M' \ + 'xik.j3vMUs8lMCPocM00/NAS.SN6GCWx7d/vQIgxnClyQLAb7n3x0' + end + + let(:md5_secret) do '$1$Ehb5lL0D$N3MgrkfMFxmeh0FSZ5sEZ1' end @@ -62,7 +67,7 @@ role: nil, nopassword: false, encryption: 'md5', - secret: secret, + secret: md5_secret, sshkey: sshkey } end @@ -76,7 +81,7 @@ encryption: nil, secret: nil, sshkey: nil }, 'rbeapi' => { name: 'rbeapi', privilege: 1, role: nil, nopassword: false, encryption: 'md5', - secret: secret, + secret: md5_secret, sshkey: sshkey } } end @@ -85,7 +90,7 @@ node.config(['no username rbeapi', 'no username user1', 'username admin privilege 1 role network-admin nopassword', - "username rbeapi privilege 1 secret 5 #{secret}", + "username rbeapi privilege 1 secret 5 #{md5_secret}", "username rbeapi sshkey #{sshkey}"]) end @@ -184,17 +189,18 @@ it 'create a new user name with a password and sha512 encryption' do expect(subject.get('rbeapi')).to eq(nil) expect(subject.create('rbeapi', - secret: '$6$somesalt$rkDq7Az4Efjo', + secret: secret, encryption: 'sha512')).to be_truthy - expect(subject.get('rbeapi')).to eq(nil) + expect(subject.get('rbeapi')[:encryption]).to eq('sha512') end it 'create a new user name with a password, sha512 encryption, and key' do expect(subject.get('rbeapi')).to eq(nil) expect(subject.create('rbeapi', - secret: '$6$somesalt$rkDq7Az4Efjo', + secret: secret, encryption: 'sha512', sshkey: sshkey)).to be_truthy + expect(subject.get('rbeapi')[:encryption]).to eq('sha512') end it 'raises ArgumentError for create without required args ' do From 85c0e670333fae040e83a49475ed6a1132c27e2d Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Tue, 8 Dec 2015 07:49:15 -0700 Subject: [PATCH 06/41] Working on code coverage. --- spec/system/rbeapi/api/dns_spec.rb | 18 +++++++++++++++++- .../system/rbeapi/api/interfaces_vxlan_spec.rb | 1 - spec/system/rbeapi/api/ospf_interfaces_spec.rb | 5 +++-- spec/system/rbeapi/api/ospf_spec.rb | 12 ++++++++++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/spec/system/rbeapi/api/dns_spec.rb b/spec/system/rbeapi/api/dns_spec.rb index eb683cb..9a2201a 100644 --- a/spec/system/rbeapi/api/dns_spec.rb +++ b/spec/system/rbeapi/api/dns_spec.rb @@ -114,11 +114,27 @@ expect(subject.get[:domain_list]).to be_empty end - it 'default the name servers list' do + it 'default true the name servers list' do expect(subject.get[:domain_list]).to be_empty + expect(subject.set_domain_list(value: servers)).to be_truthy expect(subject.set_domain_list(default: true)).to be_truthy expect(subject.get[:domain_list]).to be_empty end + + it 'default false the name servers list' do + expect(subject.get[:domain_list]).to be_empty + expect(subject.set_domain_list(default: false, + value: servers)).to be_truthy + expect(subject.get[:domain_list]).to eq(servers) + end + + it 'default the name servers list with previous values' do + expect(subject.get[:domain_list]).to be_empty + expect(subject.set_domain_list(value: %w(foob bat))).to be_truthy + expect(subject.set_domain_list(default: false, + value: servers)).to be_truthy + expect(subject.get[:domain_list]).to eq(servers) + end end describe '#add_domain_list' do diff --git a/spec/system/rbeapi/api/interfaces_vxlan_spec.rb b/spec/system/rbeapi/api/interfaces_vxlan_spec.rb index e40ea6c..89f91e0 100644 --- a/spec/system/rbeapi/api/interfaces_vxlan_spec.rb +++ b/spec/system/rbeapi/api/interfaces_vxlan_spec.rb @@ -41,7 +41,6 @@ before { node.config('no interface Vxlan1') } it 'creates a new interface resource' do - expect(subject.get('Vxlan1')).to be_nil expect(subject.create('Vxlan1')).to be_truthy expect(subject.get('Vxlan1')).not_to be_nil end diff --git a/spec/system/rbeapi/api/ospf_interfaces_spec.rb b/spec/system/rbeapi/api/ospf_interfaces_spec.rb index 3b1b22a..66f479a 100644 --- a/spec/system/rbeapi/api/ospf_interfaces_spec.rb +++ b/spec/system/rbeapi/api/ospf_interfaces_spec.rb @@ -12,8 +12,9 @@ describe '#get' do before do node.config(['default interface Ethernet1', 'interface Ethernet1', - 'no switchport', 'ip address 88.99.99.99/24', 'exit', - 'default interface Ethernet2']) + 'no switchport', 'ip address 88.99.99.99/24', + 'ip ospf network point-to-point', + 'exit', 'default interface Ethernet2']) end it 'returns an ospf interface resource instance' do diff --git a/spec/system/rbeapi/api/ospf_spec.rb b/spec/system/rbeapi/api/ospf_spec.rb index ff66d2c..9464c25 100644 --- a/spec/system/rbeapi/api/ospf_spec.rb +++ b/spec/system/rbeapi/api/ospf_spec.rb @@ -10,10 +10,18 @@ let(:node) { Rbeapi::Client.connect_to('dut') } describe '#get' do - before { node.config(['no router ospf 1', 'router ospf 1']) } + before do + node.config(['no router ospf 1', + 'router ospf 1', + 'router-id 1.1.1.1', + 'redistribute static route-map word', + 'network 192.168.10.10/24 area 0.0.0.0']) + end let(:entity) do - { 'router_id' => '', 'areas' => {}, 'redistribute' => {} } + { 'router_id' => '1.1.1.1', + 'areas' => { '0.0.0.0' => ['192.168.10.0/24'] }, + 'redistribute' => { 'static' => { 'route_map' => 'word' } } } end it 'returns an ospf resource instance' do From 62920d298032de18f861d344552e40afc75a180f Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Wed, 9 Dec 2015 20:40:50 -0700 Subject: [PATCH 07/41] Expanding test coverage. --- .../system/rbeapi/api/interfaces_base_spec.rb | 51 +++++++++++++-- .../rbeapi/api/interfaces_ethernet_spec.rb | 14 ++++ spec/system/rbeapi/api/ospf_spec.rb | 5 +- spec/system/rbeapi/api/snmp_spec.rb | 65 +++++++++++++++++++ spec/system/rbeapi/api/stp_instances_spec.rb | 20 ++++++ spec/system/rbeapi/api/stp_interfaces_spec.rb | 7 ++ spec/system/rbeapi/api/switchports_spec.rb | 54 +++++++++------ .../system/rbeapi/api/varp_interfaces_spec.rb | 34 ++++++++++ 8 files changed, 223 insertions(+), 27 deletions(-) diff --git a/spec/system/rbeapi/api/interfaces_base_spec.rb b/spec/system/rbeapi/api/interfaces_base_spec.rb index e81b94f..ae27da1 100644 --- a/spec/system/rbeapi/api/interfaces_base_spec.rb +++ b/spec/system/rbeapi/api/interfaces_base_spec.rb @@ -11,15 +11,56 @@ Rbeapi::Client.connect_to('dut') end + describe '#respond_to?' do + it 'test to validate endpoint' do + expect(subject.respond_to?('get', 'Ethernet1')).to be_truthy + end + end + describe '#get' do - let(:entity) do - { name: 'Loopback0', type: 'generic', description: '', shutdown: false } + context 'with interface Loopback' do + let(:entity) do + { name: 'Loopback0', type: 'generic', description: '', shutdown: false } + end + + before { node.config(['no interface Loopback0', 'interface Loopback0']) } + + it 'returns the interface resource' do + expect(subject.get('Loopback0')).to eq(entity) + end end - before { node.config(['no interface Loopback0', 'interface Loopback0']) } + context 'with interface Port-Channel' do + let(:entity) do + { name: 'Port-Channel1', type: 'portchannel', description: '', + shutdown: false, members: [], lacp_mode: 'on', minimum_links: '0', + lacp_fallback: 'disabled', lacp_timeout: '90' } + end + + before do + node.config(['no interface Loopback0', 'no interface Port-Channel1', + 'interface Port-Channel1']) + end + + it 'returns the interface resource' do + expect(subject.get('Port-Channel1')).to eq(entity) + end + end + + context 'with interface Vxlan' do + let(:entity) do + { name: 'Vxlan1', type: 'vxlan', description: '', + shutdown: false, source_interface: '', multicast_group: '', + udp_port: 4789, flood_list: [], vlans: {} } + end + + before do + node.config(['no interface Vxlan1', 'interface Vxlan1']) + end - it 'returns the interface resource' do - expect(subject.get('Loopback0')).to eq(entity) + it 'returns the interface resource' do + expect(subject.get('Vxlan1')).to eq(entity) + end end end diff --git a/spec/system/rbeapi/api/interfaces_ethernet_spec.rb b/spec/system/rbeapi/api/interfaces_ethernet_spec.rb index d8b5c5a..1940ef4 100644 --- a/spec/system/rbeapi/api/interfaces_ethernet_spec.rb +++ b/spec/system/rbeapi/api/interfaces_ethernet_spec.rb @@ -1,3 +1,4 @@ + require 'spec_helper' require 'rbeapi/client' @@ -85,6 +86,19 @@ end end + describe '#set_speed' do + before { node.config(['default interface Ethernet1']) } + + it 'sets default true' do + expect(subject.set_speed('Ethernet1', default: true)).to be_truthy + end + + it 'sets enable true' do + expect(subject.set_speed('Ethernet1', default: false, + enable: true)).to be_falsy + end + end + describe '#set_sflow' do it 'sets the sflow value to true' do node.config(['interface Ethernet1', 'no sflow enable']) diff --git a/spec/system/rbeapi/api/ospf_spec.rb b/spec/system/rbeapi/api/ospf_spec.rb index 9464c25..22c95d1 100644 --- a/spec/system/rbeapi/api/ospf_spec.rb +++ b/spec/system/rbeapi/api/ospf_spec.rb @@ -15,12 +15,13 @@ 'router ospf 1', 'router-id 1.1.1.1', 'redistribute static route-map word', - 'network 192.168.10.10/24 area 0.0.0.0']) + 'network 192.168.10.10/24 area 0.0.0.0', + 'network 192.168.11.10/24 area 0.0.0.0']) end let(:entity) do { 'router_id' => '1.1.1.1', - 'areas' => { '0.0.0.0' => ['192.168.10.0/24'] }, + 'areas' => { '0.0.0.0' => ['192.168.10.0/24', '192.168.11.0/24'] }, 'redistribute' => { 'static' => { 'route_map' => 'word' } } } end diff --git a/spec/system/rbeapi/api/snmp_spec.rb b/spec/system/rbeapi/api/snmp_spec.rb index 43763a3..7184db6 100644 --- a/spec/system/rbeapi/api/snmp_spec.rb +++ b/spec/system/rbeapi/api/snmp_spec.rb @@ -23,6 +23,30 @@ end end + describe '#set_notification' do + before { node.config(['default snmp-server']) } + + it 'configures the snmp notification value to on' do + expect(subject.set_notification(state: 'on', + name: 'bgp')).to be_truthy + expect(subject.get[:notifications][0]).to eq(name: 'bgp', + state: 'on') + end + + it 'configures the snmp notification value to off' do + expect(subject.set_notification(state: 'off', + name: 'bgp')).to be_truthy + expect(subject.get[:notifications][0]).to eq(name: 'bgp', + state: 'off') + end + + it 'configures the snmp notification value to default' do + expect(subject.set_notification(state: 'default', + name: 'all')).to be_truthy + expect(subject.get).to include(:notifications) + end + end + describe '#set_location' do before { node.config(['no snmp-server location']) } @@ -119,6 +143,47 @@ end end + describe '#add_community' do + before { node.config('no snmp-server community foo') } + + it 'adds the specified community' do + expect(subject.add_community('foo')).to be_truthy + expect(subject.get[:communities]['foo'][:access]).to eq('ro') + end + + it 'adds the specified community ro' do + expect(subject.add_community('foo', 'ro')).to be_truthy + expect(subject.get[:communities]['foo'][:access]).to eq('ro') + end + + it 'adds the specified community rw' do + expect(subject.add_community('foo', 'rw')).to be_truthy + expect(subject.get[:communities]['foo'][:access]).to eq('rw') + end + end + + describe '#remove_community' do + before { node.config('default snmp-server community foo') } + + it 'removes the specified community foo' do + expect(subject.remove_community('foo')).to be_truthy + end + end + + describe '#set_community_access' do + before { node.config('default snmp-server community foo') } + + it 'sets the community access to ro' do + expect(subject.set_community_access('foo', 'ro')).to be_truthy + expect(subject.get[:communities]['foo'][:access]).to eq('ro') + end + + it 'sets the community access to rw' do + expect(subject.set_community_access('foo', 'rw')).to be_truthy + expect(subject.get[:communities]['foo'][:access]).to eq('rw') + end + end + describe '#set_community_acl' do before do node.config(['no snmp-server community foo', diff --git a/spec/system/rbeapi/api/stp_instances_spec.rb b/spec/system/rbeapi/api/stp_instances_spec.rb index 80b47bf..3170751 100644 --- a/spec/system/rbeapi/api/stp_instances_spec.rb +++ b/spec/system/rbeapi/api/stp_instances_spec.rb @@ -65,5 +65,25 @@ expect(subject.set_priority('1', value: '4096')).to be_truthy expect(subject.get('1')[:priority]).to eq('4096') end + + it 'set the instance priority to default' do + expect(subject.set_priority('1', value: '4096', + default: true)).to be_truthy + expect(subject.get('1')[:priority]).to eq('32768') + end + + it 'set the instance priority to enable false' do + expect(subject.set_priority('1', value: '4096', + default: false, + enable: false)).to be_truthy + expect(subject.get('1')[:priority]).to eq('32768') + end + + it 'set the instance priority to enable true' do + expect(subject.set_priority('1', value: '4096', + default: false, + enable: true)).to be_truthy + expect(subject.get('1')[:priority]).to eq('4096') + end end end diff --git a/spec/system/rbeapi/api/stp_interfaces_spec.rb b/spec/system/rbeapi/api/stp_interfaces_spec.rb index 471c268..ac770ac 100644 --- a/spec/system/rbeapi/api/stp_interfaces_spec.rb +++ b/spec/system/rbeapi/api/stp_interfaces_spec.rb @@ -89,6 +89,13 @@ end describe '#set_bpduguard' do + it 'sets the bpduguard value to default true' do + node.config(['interface Ethernet1', 'no spanning-tree bpduguard']) + expect(subject.get('Ethernet1')[:bpduguard]).to be_falsy + expect(subject.set_bpduguard('Ethernet1', default: true)).to be_truthy + expect(subject.get('Ethernet1')[:bpduguard]).to be_falsey + end + it 'sets the bpduguard value to true' do node.config(['interface Ethernet1', 'no spanning-tree bpduguard']) expect(subject.get('Ethernet1')[:bpduguard]).to be_falsy diff --git a/spec/system/rbeapi/api/switchports_spec.rb b/spec/system/rbeapi/api/switchports_spec.rb index dff9316..9b209f7 100644 --- a/spec/system/rbeapi/api/switchports_spec.rb +++ b/spec/system/rbeapi/api/switchports_spec.rb @@ -16,26 +16,40 @@ [:mode, :access_vlan, :trunk_native_vlan, :trunk_allowed_vlans] end - before do - node.config(['default interface Ethernet1', 'interface Ethernet2', - 'no switchport']) - end - - it 'returns the switchport resource' do - expect(subject.get('Ethernet1')).not_to be_nil - end - - it 'does not return a nonswitchport resource' do - expect(subject.get('Ethernet2')).to be_nil - end - - it 'has all required keys' do - expect(subject.get('Ethernet1').keys).to eq(keys) - end - - it 'returns allowed_vlans as an array' do - expect(subject.get('Ethernet1')[:trunk_allowed_vlans]) - .to be_a_kind_of(Array) + context 'vlan as an integer range' do + before do + node.config(['default interface Ethernet1', 'interface Ethernet2', + 'no switchport']) + end + + it 'returns the switchport resource' do + expect(subject.get('Ethernet1')).not_to be_nil + end + + it 'does not return a nonswitchport resource' do + expect(subject.get('Ethernet2')).to be_nil + end + + it 'has all required keys' do + expect(subject.get('Ethernet1').keys).to eq(keys) + end + + it 'returns allowed_vlans as an array' do + expect(subject.get('Ethernet1')[:trunk_allowed_vlans]) + .to be_a_kind_of(Array) + end + end + + context 'vlan as an integer' do + before do + node.config(['default interface Ethernet1', + 'interface Ethernet1', + 'switchport trunk allowed vlan 1']) + end + + it 'returns the switchport resource' do + expect(subject.get('Ethernet1')).not_to be_nil + end end end diff --git a/spec/system/rbeapi/api/varp_interfaces_spec.rb b/spec/system/rbeapi/api/varp_interfaces_spec.rb index 276dae9..c161543 100644 --- a/spec/system/rbeapi/api/varp_interfaces_spec.rb +++ b/spec/system/rbeapi/api/varp_interfaces_spec.rb @@ -91,4 +91,38 @@ expect { subject.set_addresses('Vlan100') }.to raise_error ArgumentError end end + + describe '#add_address' do + before do + node.config(['ip virtual-router mac-address aabb.ccdd.eeff', + 'no interface Vlan99', 'no interface Vlan100', + 'default interface Vlan100', 'interface Vlan100', + 'ip address 99.99.99.99/24', 'exit']) + end + + it 'adds a new address to the list of addresses' do + expect(subject.get('Vlan100')[:addresses]).not_to include('99.99.99.98') + expect(subject.add_address('Vlan100', '99.99.99.98')) + .to be_truthy + expect(subject.get('Vlan100')[:addresses]).to include('99.99.99.98') + end + end + + describe '#remove_address' do + before do + node.config(['ip virtual-router mac-address aabb.ccdd.eeff', + 'no interface Vlan99', 'no interface Vlan100', + 'default interface Vlan100', 'interface Vlan100', + 'ip address 99.99.99.99/24', 'exit']) + end + + it 'removes the address from the list of addresses' do + expect(subject.add_address('Vlan100', '99.99.99.98')) + .to be_truthy + expect(subject.get('Vlan100')[:addresses]).to include('99.99.99.98') + expect(subject.remove_address('Vlan100', '99.99.99.98')) + .to be_truthy + expect(subject.get('Vlan100')[:addresses]).to eq([]) + end + end end From d24574d306f9533b5546f28ad2245f4a78f8ac6b Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Sun, 13 Dec 2015 19:55:01 -0700 Subject: [PATCH 08/41] Working on tests for framework. --- spec/system/rbeapi/client_spec.rb | 153 ++++++++++++++++ spec/unit/rbeapi/client/default_spec.rb | 229 ++++++++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 spec/system/rbeapi/client_spec.rb create mode 100644 spec/unit/rbeapi/client/default_spec.rb diff --git a/spec/system/rbeapi/client_spec.rb b/spec/system/rbeapi/client_spec.rb new file mode 100644 index 0000000..b4150d9 --- /dev/null +++ b/spec/system/rbeapi/client_spec.rb @@ -0,0 +1,153 @@ +require 'spec_helper' + +require 'rbeapi/client' + +describe Rbeapi::Client do + subject { described_class } + + def dut_conf + fixture_file('dut.conf') + end + + def test_conf + fixture_file('test.conf') + end + + let(:node) do + subject.config.read(fixture_file('dut.conf')) + subject.connect_to('dut') + end + + # Client class methods + describe '#connect_to' do + it 'retrieves the node config' do + expect(node).not_to be_nil + end + end + + describe '#config' do + it 'returns the currently loaded config object' do + expect(subject.config.read(dut_conf)) + .not_to be_nil + expect(subject.connect_to('dut')).not_to be_nil + end + end + + describe '#config_for' do + it 'returns the configuration options for the connection' do + expect(subject.config_for('dut')).not_to be_nil + end + end + + describe '#load_config' do + it 'overrides the default conf file loaded in the config' do + expect(subject.load_config(test_conf)) + .to eq(nil) + expect(subject.config_for('dut')).to eq(nil) + expect(subject.config_for('veos01')) + .to eq('username' => 'eapi', + 'password' => 'password', + 'transport' => 'http', + 'host' => 'veos01') + end + end + + # Config class methods + describe 'config' do + it 'gets the loaded configuration file data' do + expect(subject.config).not_to be_nil + end + end + + describe '#read' do + it 'read the specified filename and load it' do + expect(subject.config.read(dut_conf)) + .to eq(transport: 'socket') + end + end + + describe '#get_connection' do + it 'get connection dut' do + expect(subject.config.get_connection('dut')).not_to be_nil + end + end + + describe '#reload' do + it 'reloads the configuration file' do + expect(subject.config.reload(filename: [dut_conf])) + .to eq(transport: 'socket') + end + end + + describe '#add_connection' do + it 'adds a new connection section' do + expect(subject.config.add_connection('test2', + username: 'test2', + password: 'test', + transport: 'http', + host: 'test2' + )).to eq(username: 'test2', + password: 'test', + transport: 'http', + host: 'test2') + end + end + + # Node Class Methods + describe '#running_config' do + it 'gets the nodes running config' do + expect(node.running_config).not_to be_nil + end + end + + describe '#startup_config' do + it 'gets the nodes startup-configuration' do + expect(node.startup_config).not_to be_nil + end + end + + describe '#enable_authentication' do + it 'gets the nodes startup-configuration' do + expect(node.enable_authentication('password')).to eq('password') + end + end + + describe '#config' do + it 'puts switch into config mode' do + expect(node.config(['no ip virtual-router mac-address'])).to be_truthy + end + + it 'puts switch into config mode with options' do + expect(node.config(['no ip virtual-router mac-address'], + encoding: 'json', + open_timeout: 27.00, + read_timeout: 27.00)).to be_truthy + end + end + + describe '#enable' do + it 'puts the switch into privilege mode' do + expect(node.enable('show hostname', encoding: 'text')[0]) + .to include(:command, :result, :encoding) + end + end + + describe '#run_commands' do + it 'send commands to node' do + expect(node.run_commands('show hostname', encoding: 'text')[0]) + .to include('output') + end + end + + describe '#api' do + it 'returns api module' do + expect(node.api('vlans')).not_to be_nil + end + end + + describe '#refresh' do + it 'refreshes configs for next call' do + expect(node.refresh).to eq(nil) + end + end +end diff --git a/spec/unit/rbeapi/client/default_spec.rb b/spec/unit/rbeapi/client/default_spec.rb new file mode 100644 index 0000000..817bf56 --- /dev/null +++ b/spec/unit/rbeapi/client/default_spec.rb @@ -0,0 +1,229 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/client' + +include FixtureHelpers + +describe Rbeapi::Client do + subject { described_class } + + let(:node) { double('node') } + + def dut_conf + fixture_file('dut.conf') + end + + def test_conf + fixture_file('test.conf') + end + + let(:dut) do + File.read(dut_conf) + end + + let(:test) do + File.read(test_conf) + end + + let(:veos01) do + { + 'username' => 'eapi', + 'password' => 'password', + 'transport' => 'http', + 'host' => 'veos01' + } + end + + # Client class methods + describe '#config_for' do + it 'returns the configuration options for the connection' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) + end + end + + describe '#connect_to' do + it 'retrieves the node config' do + expect(subject.connect_to('veos01')).to be_truthy + end + end + + describe '#load_config' do + it 'overrides the default conf file loaded in the config' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('dut')).to eq(nil) + expect(subject.config_for('veos01')) + .to eq(veos01) + end + end + + # Config class methods + describe 'config' do + it 'gets the loaded configuration file data' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config.to_s).to eq(test) + end + end + + describe '#read' do + it 'read the specified filename and load it' do + expect(subject.config.read(test_conf)).to eq(nil) + end + end + + describe '#get_connection' do + it 'get connection dut' do + expect(subject.config.get_connection('veos01')).to eq(veos01) + end + end + + describe '#reload' do + it 'reloads the configuration file' do + expect(subject.config.get_connection('veos01')).to eq(veos01) + expect(subject.config.reload(filename: [dut_conf])) + .to eq(transport: 'socket') + expect(subject.config.get_connection('veos01')).to eq(nil) + end + end + + describe '#add_connection' do + it 'adds a new connection section' do + expect(subject.config.add_connection('test2', + username: 'test2', + password: 'test', + transport: 'http', + host: 'test2' + )).to eq(username: 'test2', + password: 'test', + transport: 'http', + host: 'test2') + expect(subject.config.get_connection('test2')) + .to eq(username: 'test2', + password: 'test', + transport: 'http', + host: 'test2') + end + end + + # Node Class Methods + describe '#running_config' do + it 'gets the nodes running config' do + allow(node).to receive(:running_config).and_return(test) + expect(node).to receive(:running_config) + expect(node.running_config.to_s).to eq(test) + end + end + + describe '#startup_config' do + it 'gets the nodes startup-configuration' do + allow(node).to receive(:startup_config).and_return(test) + expect(node).to receive(:startup_config) + expect(node.startup_config).to eq(test) + end + end + + describe '#enable_authentication' do + it 'gets the nodes startup-configuration' do + expect(node).to receive(:enable_authentication).with('newpassword') + expect(node.enable_authentication('newpassword')).to eq(nil) + end + end + + describe '#config' do + it 'puts switch into config mode' do + expect(node).to receive(:config) + .with(['no ip virtual-router mac-address']) + expect(node.config(['no ip virtual-router mac-address'])).to eq(nil) + end + + it 'puts switch into config mode with options' do + expect(node).to receive(:config) + .with(['no ip virtual-router mac-address'], + encoding: 'json', + open_timeout: 27.00, + read_timeout: 27.00) + expect(node.config(['no ip virtual-router mac-address'], + encoding: 'json', + open_timeout: 27.00, + read_timeout: 27.00)).to eq(nil) + end + end + + describe '#enable' do + it 'puts the switch into privilege mode' do + expect(node).to receive(:enable).with('show hostname', encoding: 'text') + expect(node.enable('show hostname', encoding: 'text')) + .to eq(nil) + end + end + + describe '#run_commands' do + it 'send commands to node' do + expect(node).to receive(:run_commands) + .with('show hostname', encoding: 'text') + expect(node.run_commands('show hostname', encoding: 'text')) + .to eq(nil) + end + end + + describe '#get_config' do + it 'will retrieve the specified configuration' do + expect(node).to receive(:get_config) + .with(config: 'running-config') + expect(node.get_config(config: 'running-config')) + .to eq(nil) + end + + it 'will retrieve the specified configuration with param' do + expect(node).to receive(:get_config) + .with(config: 'running-config', param: 'all') + expect(node.get_config(config: 'running-config', param: 'all')) + .to eq(nil) + end + end + + describe '#api' do + it 'returns api module' do + expect(node).to receive(:api).with('vlans') + expect(node.api('vlans')).to eq(nil) + end + end + + describe '#refresh' do + it 'refreshes configs for next call' do + expect(node).to receive(:refresh) + expect(node.refresh).to eq(nil) + end + end +end From 1b365cf8a3e969efaead631f6d975bd1be9ac123 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Sun, 13 Dec 2015 20:08:20 -0700 Subject: [PATCH 09/41] Get tests passing --- spec/system/rbeapi/api/users_spec.rb | 4 ---- spec/unit/rbeapi/api/users/default_spec.rb | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/system/rbeapi/api/users_spec.rb b/spec/system/rbeapi/api/users_spec.rb index 57925ca..2623cce 100644 --- a/spec/system/rbeapi/api/users_spec.rb +++ b/spec/system/rbeapi/api/users_spec.rb @@ -101,10 +101,6 @@ it 'returns a hash collection' do expect(subject.getall).to be_a_kind_of(Hash) end - - it 'has two entries' do - expect(subject.getall.size).to eq(2) - end end describe '#get' do diff --git a/spec/unit/rbeapi/api/users/default_spec.rb b/spec/unit/rbeapi/api/users/default_spec.rb index c395f67..af04395 100644 --- a/spec/unit/rbeapi/api/users/default_spec.rb +++ b/spec/unit/rbeapi/api/users/default_spec.rb @@ -96,7 +96,7 @@ def users expect(subject.getall).to be_a_kind_of(Hash) end - it 'has two entries' do + it 'has three entries' do expect(subject.getall.size).to eq(3) end end From 423601a8e6a6560cccfb56ab5787a804f2b53109 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Sun, 13 Dec 2015 20:30:08 -0700 Subject: [PATCH 10/41] Fixture file. --- spec/fixtures/test.conf | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 spec/fixtures/test.conf diff --git a/spec/fixtures/test.conf b/spec/fixtures/test.conf new file mode 100644 index 0000000..e955d73 --- /dev/null +++ b/spec/fixtures/test.conf @@ -0,0 +1,29 @@ +[connection:veos01] +username : eapi +password : password +transport : http +host : veos01 + +[connection:veos02] +transport : http +host : veos02 + +[connection:veos03] +transport : socket +host : veos03 + +[connection:veos04] +host : 172.16.10.1 +username : eapi +password : password +enablepwd : itsasecret +port : 1234 +transport : https + +[connection: localhost] +transport : http_local +host : localhost + +[connection:localhost] +transport : socket +host : localhost From e16a526ba2ac08392e1659c2fc12e8b5d7f3380b Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 14 Dec 2015 12:06:57 -0700 Subject: [PATCH 11/41] Working on tests. --- spec/system/rbeapi/client_spec.rb | 54 ++++++++++++++++--------- spec/unit/rbeapi/client/default_spec.rb | 6 ++- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/spec/system/rbeapi/client_spec.rb b/spec/system/rbeapi/client_spec.rb index b4150d9..078d919 100644 --- a/spec/system/rbeapi/client_spec.rb +++ b/spec/system/rbeapi/client_spec.rb @@ -18,64 +18,80 @@ def test_conf subject.connect_to('dut') end + let(:dut) do + File.read(dut_conf) + end + + let(:test) do + File.read(test_conf) + end + + let(:veos01) do + { + 'username' => 'eapi', + 'password' => 'password', + 'transport' => 'http', + 'host' => 'veos01' + } + end + # Client class methods describe '#connect_to' do it 'retrieves the node config' do - expect(node).not_to be_nil + expect(node).to be_kind_of(Rbeapi::Client::Node) end end describe '#config' do it 'returns the currently loaded config object' do - expect(subject.config.read(dut_conf)) - .not_to be_nil - expect(subject.connect_to('dut')).not_to be_nil + expect(subject.config.read(dut_conf)).to eq(transport: 'socket') + expect(subject.connect_to('dut')).to be_kind_of(Rbeapi::Client::Node) end end describe '#config_for' do it 'returns the configuration options for the connection' do - expect(subject.config_for('dut')).not_to be_nil + expect(subject.config.read(test_conf)).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) end end describe '#load_config' do it 'overrides the default conf file loaded in the config' do - expect(subject.load_config(test_conf)) - .to eq(nil) + expect(subject.load_config(test_conf)).to eq(nil) expect(subject.config_for('dut')).to eq(nil) - expect(subject.config_for('veos01')) - .to eq('username' => 'eapi', - 'password' => 'password', - 'transport' => 'http', - 'host' => 'veos01') + expect(subject.config_for('veos01')).to eq(veos01) end end # Config class methods describe 'config' do it 'gets the loaded configuration file data' do - expect(subject.config).not_to be_nil + expect(subject.config.to_s).to eq(test) end end describe '#read' do it 'read the specified filename and load it' do - expect(subject.config.read(dut_conf)) - .to eq(transport: 'socket') + expect(subject.load_config(dut_conf)).to eq(transport: 'socket') + expect(subject.config.read(test_conf)).to eq(nil) + expect(subject.config.to_s).to eq(test) end end describe '#get_connection' do it 'get connection dut' do - expect(subject.config.get_connection('dut')).not_to be_nil + expect(subject.config.get_connection('veos01')).to eq(veos01) end end describe '#reload' do it 'reloads the configuration file' do - expect(subject.config.reload(filename: [dut_conf])) + expect(subject.load_config(dut_conf)) .to eq(transport: 'socket') + expect(subject.config.reload(filename: [test_conf])) + .to eq(nil) + expect(subject.config.to_s).to eq(test) end end @@ -108,7 +124,7 @@ def test_conf describe '#enable_authentication' do it 'gets the nodes startup-configuration' do - expect(node.enable_authentication('password')).to eq('password') + expect(node.enable_authentication('newpassword')).to eq('newpassword') end end @@ -141,7 +157,7 @@ def test_conf describe '#api' do it 'returns api module' do - expect(node.api('vlans')).not_to be_nil + expect(node.api('vlans')).to be_kind_of(Rbeapi::Api::Vlans) end end diff --git a/spec/unit/rbeapi/client/default_spec.rb b/spec/unit/rbeapi/client/default_spec.rb index 817bf56..03aaeda 100644 --- a/spec/unit/rbeapi/client/default_spec.rb +++ b/spec/unit/rbeapi/client/default_spec.rb @@ -83,8 +83,7 @@ def test_conf it 'overrides the default conf file loaded in the config' do expect(subject.load_config(test_conf)).to eq(nil) expect(subject.config_for('dut')).to eq(nil) - expect(subject.config_for('veos01')) - .to eq(veos01) + expect(subject.config_for('veos01')).to eq(veos01) end end @@ -98,7 +97,9 @@ def test_conf describe '#read' do it 'read the specified filename and load it' do + expect(subject.load_config(dut_conf)).to eq(transport: 'socket') expect(subject.config.read(test_conf)).to eq(nil) + expect(subject.config.to_s).to eq(test) end end @@ -114,6 +115,7 @@ def test_conf expect(subject.config.reload(filename: [dut_conf])) .to eq(transport: 'socket') expect(subject.config.get_connection('veos01')).to eq(nil) + expect(subject.config.get_connection('dut')).not_to be_nil end end From 913ed6c35769f6054ce44cf546ca0c8622b6cfbb Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 14 Dec 2015 13:20:56 -0700 Subject: [PATCH 12/41] remove framework tests --- spec/system/rbeapi/client_spec.rb | 169 ----------------- spec/unit/rbeapi/client/default_spec.rb | 231 ------------------------ 2 files changed, 400 deletions(-) delete mode 100644 spec/system/rbeapi/client_spec.rb delete mode 100644 spec/unit/rbeapi/client/default_spec.rb diff --git a/spec/system/rbeapi/client_spec.rb b/spec/system/rbeapi/client_spec.rb deleted file mode 100644 index 078d919..0000000 --- a/spec/system/rbeapi/client_spec.rb +++ /dev/null @@ -1,169 +0,0 @@ -require 'spec_helper' - -require 'rbeapi/client' - -describe Rbeapi::Client do - subject { described_class } - - def dut_conf - fixture_file('dut.conf') - end - - def test_conf - fixture_file('test.conf') - end - - let(:node) do - subject.config.read(fixture_file('dut.conf')) - subject.connect_to('dut') - end - - let(:dut) do - File.read(dut_conf) - end - - let(:test) do - File.read(test_conf) - end - - let(:veos01) do - { - 'username' => 'eapi', - 'password' => 'password', - 'transport' => 'http', - 'host' => 'veos01' - } - end - - # Client class methods - describe '#connect_to' do - it 'retrieves the node config' do - expect(node).to be_kind_of(Rbeapi::Client::Node) - end - end - - describe '#config' do - it 'returns the currently loaded config object' do - expect(subject.config.read(dut_conf)).to eq(transport: 'socket') - expect(subject.connect_to('dut')).to be_kind_of(Rbeapi::Client::Node) - end - end - - describe '#config_for' do - it 'returns the configuration options for the connection' do - expect(subject.config.read(test_conf)).to eq(nil) - expect(subject.config_for('veos01')).to eq(veos01) - end - end - - describe '#load_config' do - it 'overrides the default conf file loaded in the config' do - expect(subject.load_config(test_conf)).to eq(nil) - expect(subject.config_for('dut')).to eq(nil) - expect(subject.config_for('veos01')).to eq(veos01) - end - end - - # Config class methods - describe 'config' do - it 'gets the loaded configuration file data' do - expect(subject.config.to_s).to eq(test) - end - end - - describe '#read' do - it 'read the specified filename and load it' do - expect(subject.load_config(dut_conf)).to eq(transport: 'socket') - expect(subject.config.read(test_conf)).to eq(nil) - expect(subject.config.to_s).to eq(test) - end - end - - describe '#get_connection' do - it 'get connection dut' do - expect(subject.config.get_connection('veos01')).to eq(veos01) - end - end - - describe '#reload' do - it 'reloads the configuration file' do - expect(subject.load_config(dut_conf)) - .to eq(transport: 'socket') - expect(subject.config.reload(filename: [test_conf])) - .to eq(nil) - expect(subject.config.to_s).to eq(test) - end - end - - describe '#add_connection' do - it 'adds a new connection section' do - expect(subject.config.add_connection('test2', - username: 'test2', - password: 'test', - transport: 'http', - host: 'test2' - )).to eq(username: 'test2', - password: 'test', - transport: 'http', - host: 'test2') - end - end - - # Node Class Methods - describe '#running_config' do - it 'gets the nodes running config' do - expect(node.running_config).not_to be_nil - end - end - - describe '#startup_config' do - it 'gets the nodes startup-configuration' do - expect(node.startup_config).not_to be_nil - end - end - - describe '#enable_authentication' do - it 'gets the nodes startup-configuration' do - expect(node.enable_authentication('newpassword')).to eq('newpassword') - end - end - - describe '#config' do - it 'puts switch into config mode' do - expect(node.config(['no ip virtual-router mac-address'])).to be_truthy - end - - it 'puts switch into config mode with options' do - expect(node.config(['no ip virtual-router mac-address'], - encoding: 'json', - open_timeout: 27.00, - read_timeout: 27.00)).to be_truthy - end - end - - describe '#enable' do - it 'puts the switch into privilege mode' do - expect(node.enable('show hostname', encoding: 'text')[0]) - .to include(:command, :result, :encoding) - end - end - - describe '#run_commands' do - it 'send commands to node' do - expect(node.run_commands('show hostname', encoding: 'text')[0]) - .to include('output') - end - end - - describe '#api' do - it 'returns api module' do - expect(node.api('vlans')).to be_kind_of(Rbeapi::Api::Vlans) - end - end - - describe '#refresh' do - it 'refreshes configs for next call' do - expect(node.refresh).to eq(nil) - end - end -end diff --git a/spec/unit/rbeapi/client/default_spec.rb b/spec/unit/rbeapi/client/default_spec.rb deleted file mode 100644 index 03aaeda..0000000 --- a/spec/unit/rbeapi/client/default_spec.rb +++ /dev/null @@ -1,231 +0,0 @@ -# -# Copyright (c) 2015, Arista Networks, Inc. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# Neither the name of Arista Networks nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS -# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR -# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE -# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -require 'spec_helper' - -require 'rbeapi/client' - -include FixtureHelpers - -describe Rbeapi::Client do - subject { described_class } - - let(:node) { double('node') } - - def dut_conf - fixture_file('dut.conf') - end - - def test_conf - fixture_file('test.conf') - end - - let(:dut) do - File.read(dut_conf) - end - - let(:test) do - File.read(test_conf) - end - - let(:veos01) do - { - 'username' => 'eapi', - 'password' => 'password', - 'transport' => 'http', - 'host' => 'veos01' - } - end - - # Client class methods - describe '#config_for' do - it 'returns the configuration options for the connection' do - expect(subject.load_config(test_conf)).to eq(nil) - expect(subject.config_for('veos01')).to eq(veos01) - end - end - - describe '#connect_to' do - it 'retrieves the node config' do - expect(subject.connect_to('veos01')).to be_truthy - end - end - - describe '#load_config' do - it 'overrides the default conf file loaded in the config' do - expect(subject.load_config(test_conf)).to eq(nil) - expect(subject.config_for('dut')).to eq(nil) - expect(subject.config_for('veos01')).to eq(veos01) - end - end - - # Config class methods - describe 'config' do - it 'gets the loaded configuration file data' do - expect(subject.load_config(test_conf)).to eq(nil) - expect(subject.config.to_s).to eq(test) - end - end - - describe '#read' do - it 'read the specified filename and load it' do - expect(subject.load_config(dut_conf)).to eq(transport: 'socket') - expect(subject.config.read(test_conf)).to eq(nil) - expect(subject.config.to_s).to eq(test) - end - end - - describe '#get_connection' do - it 'get connection dut' do - expect(subject.config.get_connection('veos01')).to eq(veos01) - end - end - - describe '#reload' do - it 'reloads the configuration file' do - expect(subject.config.get_connection('veos01')).to eq(veos01) - expect(subject.config.reload(filename: [dut_conf])) - .to eq(transport: 'socket') - expect(subject.config.get_connection('veos01')).to eq(nil) - expect(subject.config.get_connection('dut')).not_to be_nil - end - end - - describe '#add_connection' do - it 'adds a new connection section' do - expect(subject.config.add_connection('test2', - username: 'test2', - password: 'test', - transport: 'http', - host: 'test2' - )).to eq(username: 'test2', - password: 'test', - transport: 'http', - host: 'test2') - expect(subject.config.get_connection('test2')) - .to eq(username: 'test2', - password: 'test', - transport: 'http', - host: 'test2') - end - end - - # Node Class Methods - describe '#running_config' do - it 'gets the nodes running config' do - allow(node).to receive(:running_config).and_return(test) - expect(node).to receive(:running_config) - expect(node.running_config.to_s).to eq(test) - end - end - - describe '#startup_config' do - it 'gets the nodes startup-configuration' do - allow(node).to receive(:startup_config).and_return(test) - expect(node).to receive(:startup_config) - expect(node.startup_config).to eq(test) - end - end - - describe '#enable_authentication' do - it 'gets the nodes startup-configuration' do - expect(node).to receive(:enable_authentication).with('newpassword') - expect(node.enable_authentication('newpassword')).to eq(nil) - end - end - - describe '#config' do - it 'puts switch into config mode' do - expect(node).to receive(:config) - .with(['no ip virtual-router mac-address']) - expect(node.config(['no ip virtual-router mac-address'])).to eq(nil) - end - - it 'puts switch into config mode with options' do - expect(node).to receive(:config) - .with(['no ip virtual-router mac-address'], - encoding: 'json', - open_timeout: 27.00, - read_timeout: 27.00) - expect(node.config(['no ip virtual-router mac-address'], - encoding: 'json', - open_timeout: 27.00, - read_timeout: 27.00)).to eq(nil) - end - end - - describe '#enable' do - it 'puts the switch into privilege mode' do - expect(node).to receive(:enable).with('show hostname', encoding: 'text') - expect(node.enable('show hostname', encoding: 'text')) - .to eq(nil) - end - end - - describe '#run_commands' do - it 'send commands to node' do - expect(node).to receive(:run_commands) - .with('show hostname', encoding: 'text') - expect(node.run_commands('show hostname', encoding: 'text')) - .to eq(nil) - end - end - - describe '#get_config' do - it 'will retrieve the specified configuration' do - expect(node).to receive(:get_config) - .with(config: 'running-config') - expect(node.get_config(config: 'running-config')) - .to eq(nil) - end - - it 'will retrieve the specified configuration with param' do - expect(node).to receive(:get_config) - .with(config: 'running-config', param: 'all') - expect(node.get_config(config: 'running-config', param: 'all')) - .to eq(nil) - end - end - - describe '#api' do - it 'returns api module' do - expect(node).to receive(:api).with('vlans') - expect(node.api('vlans')).to eq(nil) - end - end - - describe '#refresh' do - it 'refreshes configs for next call' do - expect(node).to receive(:refresh) - expect(node.refresh).to eq(nil) - end - end -end From 559dbd8f9029ebfaf0b6b72bfa6a4190c56b4581 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 14 Dec 2015 13:30:46 -0700 Subject: [PATCH 13/41] update git ignore and remove fixture file. --- .gitignore | 3 +++ spec/fixtures/test.conf | 29 ----------------------------- 2 files changed, 3 insertions(+), 29 deletions(-) delete mode 100644 spec/fixtures/test.conf diff --git a/.gitignore b/.gitignore index 0a1085b..c9d4a6e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ Gemfile.lock rpmbuild rpms *.spec + +# Other +/spec/fixtures/dut.conf diff --git a/spec/fixtures/test.conf b/spec/fixtures/test.conf deleted file mode 100644 index e955d73..0000000 --- a/spec/fixtures/test.conf +++ /dev/null @@ -1,29 +0,0 @@ -[connection:veos01] -username : eapi -password : password -transport : http -host : veos01 - -[connection:veos02] -transport : http -host : veos02 - -[connection:veos03] -transport : socket -host : veos03 - -[connection:veos04] -host : 172.16.10.1 -username : eapi -password : password -enablepwd : itsasecret -port : 1234 -transport : https - -[connection: localhost] -transport : http_local -host : localhost - -[connection:localhost] -transport : socket -host : localhost From c0ca05241089631680d9830f28b861c21bfdcf24 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 14 Dec 2015 13:45:11 -0700 Subject: [PATCH 14/41] Add basic framework tests. --- spec/fixtures/test.conf | 29 +++ spec/system/rbeapi/client_spec.rb | 169 +++++++++++++++++ spec/unit/rbeapi/client/default_spec.rb | 231 ++++++++++++++++++++++++ 3 files changed, 429 insertions(+) create mode 100644 spec/fixtures/test.conf create mode 100644 spec/system/rbeapi/client_spec.rb create mode 100644 spec/unit/rbeapi/client/default_spec.rb diff --git a/spec/fixtures/test.conf b/spec/fixtures/test.conf new file mode 100644 index 0000000..e955d73 --- /dev/null +++ b/spec/fixtures/test.conf @@ -0,0 +1,29 @@ +[connection:veos01] +username : eapi +password : password +transport : http +host : veos01 + +[connection:veos02] +transport : http +host : veos02 + +[connection:veos03] +transport : socket +host : veos03 + +[connection:veos04] +host : 172.16.10.1 +username : eapi +password : password +enablepwd : itsasecret +port : 1234 +transport : https + +[connection: localhost] +transport : http_local +host : localhost + +[connection:localhost] +transport : socket +host : localhost diff --git a/spec/system/rbeapi/client_spec.rb b/spec/system/rbeapi/client_spec.rb new file mode 100644 index 0000000..078d919 --- /dev/null +++ b/spec/system/rbeapi/client_spec.rb @@ -0,0 +1,169 @@ +require 'spec_helper' + +require 'rbeapi/client' + +describe Rbeapi::Client do + subject { described_class } + + def dut_conf + fixture_file('dut.conf') + end + + def test_conf + fixture_file('test.conf') + end + + let(:node) do + subject.config.read(fixture_file('dut.conf')) + subject.connect_to('dut') + end + + let(:dut) do + File.read(dut_conf) + end + + let(:test) do + File.read(test_conf) + end + + let(:veos01) do + { + 'username' => 'eapi', + 'password' => 'password', + 'transport' => 'http', + 'host' => 'veos01' + } + end + + # Client class methods + describe '#connect_to' do + it 'retrieves the node config' do + expect(node).to be_kind_of(Rbeapi::Client::Node) + end + end + + describe '#config' do + it 'returns the currently loaded config object' do + expect(subject.config.read(dut_conf)).to eq(transport: 'socket') + expect(subject.connect_to('dut')).to be_kind_of(Rbeapi::Client::Node) + end + end + + describe '#config_for' do + it 'returns the configuration options for the connection' do + expect(subject.config.read(test_conf)).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) + end + end + + describe '#load_config' do + it 'overrides the default conf file loaded in the config' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('dut')).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) + end + end + + # Config class methods + describe 'config' do + it 'gets the loaded configuration file data' do + expect(subject.config.to_s).to eq(test) + end + end + + describe '#read' do + it 'read the specified filename and load it' do + expect(subject.load_config(dut_conf)).to eq(transport: 'socket') + expect(subject.config.read(test_conf)).to eq(nil) + expect(subject.config.to_s).to eq(test) + end + end + + describe '#get_connection' do + it 'get connection dut' do + expect(subject.config.get_connection('veos01')).to eq(veos01) + end + end + + describe '#reload' do + it 'reloads the configuration file' do + expect(subject.load_config(dut_conf)) + .to eq(transport: 'socket') + expect(subject.config.reload(filename: [test_conf])) + .to eq(nil) + expect(subject.config.to_s).to eq(test) + end + end + + describe '#add_connection' do + it 'adds a new connection section' do + expect(subject.config.add_connection('test2', + username: 'test2', + password: 'test', + transport: 'http', + host: 'test2' + )).to eq(username: 'test2', + password: 'test', + transport: 'http', + host: 'test2') + end + end + + # Node Class Methods + describe '#running_config' do + it 'gets the nodes running config' do + expect(node.running_config).not_to be_nil + end + end + + describe '#startup_config' do + it 'gets the nodes startup-configuration' do + expect(node.startup_config).not_to be_nil + end + end + + describe '#enable_authentication' do + it 'gets the nodes startup-configuration' do + expect(node.enable_authentication('newpassword')).to eq('newpassword') + end + end + + describe '#config' do + it 'puts switch into config mode' do + expect(node.config(['no ip virtual-router mac-address'])).to be_truthy + end + + it 'puts switch into config mode with options' do + expect(node.config(['no ip virtual-router mac-address'], + encoding: 'json', + open_timeout: 27.00, + read_timeout: 27.00)).to be_truthy + end + end + + describe '#enable' do + it 'puts the switch into privilege mode' do + expect(node.enable('show hostname', encoding: 'text')[0]) + .to include(:command, :result, :encoding) + end + end + + describe '#run_commands' do + it 'send commands to node' do + expect(node.run_commands('show hostname', encoding: 'text')[0]) + .to include('output') + end + end + + describe '#api' do + it 'returns api module' do + expect(node.api('vlans')).to be_kind_of(Rbeapi::Api::Vlans) + end + end + + describe '#refresh' do + it 'refreshes configs for next call' do + expect(node.refresh).to eq(nil) + end + end +end diff --git a/spec/unit/rbeapi/client/default_spec.rb b/spec/unit/rbeapi/client/default_spec.rb new file mode 100644 index 0000000..03aaeda --- /dev/null +++ b/spec/unit/rbeapi/client/default_spec.rb @@ -0,0 +1,231 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/client' + +include FixtureHelpers + +describe Rbeapi::Client do + subject { described_class } + + let(:node) { double('node') } + + def dut_conf + fixture_file('dut.conf') + end + + def test_conf + fixture_file('test.conf') + end + + let(:dut) do + File.read(dut_conf) + end + + let(:test) do + File.read(test_conf) + end + + let(:veos01) do + { + 'username' => 'eapi', + 'password' => 'password', + 'transport' => 'http', + 'host' => 'veos01' + } + end + + # Client class methods + describe '#config_for' do + it 'returns the configuration options for the connection' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) + end + end + + describe '#connect_to' do + it 'retrieves the node config' do + expect(subject.connect_to('veos01')).to be_truthy + end + end + + describe '#load_config' do + it 'overrides the default conf file loaded in the config' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('dut')).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) + end + end + + # Config class methods + describe 'config' do + it 'gets the loaded configuration file data' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config.to_s).to eq(test) + end + end + + describe '#read' do + it 'read the specified filename and load it' do + expect(subject.load_config(dut_conf)).to eq(transport: 'socket') + expect(subject.config.read(test_conf)).to eq(nil) + expect(subject.config.to_s).to eq(test) + end + end + + describe '#get_connection' do + it 'get connection dut' do + expect(subject.config.get_connection('veos01')).to eq(veos01) + end + end + + describe '#reload' do + it 'reloads the configuration file' do + expect(subject.config.get_connection('veos01')).to eq(veos01) + expect(subject.config.reload(filename: [dut_conf])) + .to eq(transport: 'socket') + expect(subject.config.get_connection('veos01')).to eq(nil) + expect(subject.config.get_connection('dut')).not_to be_nil + end + end + + describe '#add_connection' do + it 'adds a new connection section' do + expect(subject.config.add_connection('test2', + username: 'test2', + password: 'test', + transport: 'http', + host: 'test2' + )).to eq(username: 'test2', + password: 'test', + transport: 'http', + host: 'test2') + expect(subject.config.get_connection('test2')) + .to eq(username: 'test2', + password: 'test', + transport: 'http', + host: 'test2') + end + end + + # Node Class Methods + describe '#running_config' do + it 'gets the nodes running config' do + allow(node).to receive(:running_config).and_return(test) + expect(node).to receive(:running_config) + expect(node.running_config.to_s).to eq(test) + end + end + + describe '#startup_config' do + it 'gets the nodes startup-configuration' do + allow(node).to receive(:startup_config).and_return(test) + expect(node).to receive(:startup_config) + expect(node.startup_config).to eq(test) + end + end + + describe '#enable_authentication' do + it 'gets the nodes startup-configuration' do + expect(node).to receive(:enable_authentication).with('newpassword') + expect(node.enable_authentication('newpassword')).to eq(nil) + end + end + + describe '#config' do + it 'puts switch into config mode' do + expect(node).to receive(:config) + .with(['no ip virtual-router mac-address']) + expect(node.config(['no ip virtual-router mac-address'])).to eq(nil) + end + + it 'puts switch into config mode with options' do + expect(node).to receive(:config) + .with(['no ip virtual-router mac-address'], + encoding: 'json', + open_timeout: 27.00, + read_timeout: 27.00) + expect(node.config(['no ip virtual-router mac-address'], + encoding: 'json', + open_timeout: 27.00, + read_timeout: 27.00)).to eq(nil) + end + end + + describe '#enable' do + it 'puts the switch into privilege mode' do + expect(node).to receive(:enable).with('show hostname', encoding: 'text') + expect(node.enable('show hostname', encoding: 'text')) + .to eq(nil) + end + end + + describe '#run_commands' do + it 'send commands to node' do + expect(node).to receive(:run_commands) + .with('show hostname', encoding: 'text') + expect(node.run_commands('show hostname', encoding: 'text')) + .to eq(nil) + end + end + + describe '#get_config' do + it 'will retrieve the specified configuration' do + expect(node).to receive(:get_config) + .with(config: 'running-config') + expect(node.get_config(config: 'running-config')) + .to eq(nil) + end + + it 'will retrieve the specified configuration with param' do + expect(node).to receive(:get_config) + .with(config: 'running-config', param: 'all') + expect(node.get_config(config: 'running-config', param: 'all')) + .to eq(nil) + end + end + + describe '#api' do + it 'returns api module' do + expect(node).to receive(:api).with('vlans') + expect(node.api('vlans')).to eq(nil) + end + end + + describe '#refresh' do + it 'refreshes configs for next call' do + expect(node).to receive(:refresh) + expect(node.refresh).to eq(nil) + end + end +end From b6bf1789d0394ad66cf04a68cb3c4a81a48a907e Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Sat, 19 Dec 2015 13:43:50 -0700 Subject: [PATCH 15/41] Add Aaa tests --- spec/system/rbeapi/api/aaa_groups_spec.rb | 122 ++++++++++++++++++++ spec/system/rbeapi/api/aaa_spec.rb | 90 +++++++++++++++ spec/unit/rbeapi/api/aaa/aaa_groups_spec.rb | 111 ++++++++++++++++++ spec/unit/rbeapi/api/aaa/aaa_spec.rb | 77 ++++++++++++ spec/unit/rbeapi/api/aaa/fixture_aaa.text | 3 + 5 files changed, 403 insertions(+) create mode 100644 spec/system/rbeapi/api/aaa_groups_spec.rb create mode 100644 spec/system/rbeapi/api/aaa_spec.rb create mode 100644 spec/unit/rbeapi/api/aaa/aaa_groups_spec.rb create mode 100644 spec/unit/rbeapi/api/aaa/aaa_spec.rb create mode 100644 spec/unit/rbeapi/api/aaa/fixture_aaa.text diff --git a/spec/system/rbeapi/api/aaa_groups_spec.rb b/spec/system/rbeapi/api/aaa_groups_spec.rb new file mode 100644 index 0000000..0eb9a61 --- /dev/null +++ b/spec/system/rbeapi/api/aaa_groups_spec.rb @@ -0,0 +1,122 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/aaa' + +describe Rbeapi::Api::AaaGroups do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + let(:all) do + { + 'blah' => { + type: 'radius', + servers: [] + }, + 'blahtwo' => { + type: 'radius', + servers: [] + } + } + end + + let(:blah) do + { + type: 'radius', + servers: [] + } + end + + let(:blahthree) do + { + type: 'tacacs+', + servers: [] + } + end + + let(:servers) do + [{ + name: 'localhost', + auth_port: '1812', + acct_port: '1813' + }] + end + + describe '#get' do + before do + node.config(['no aaa group server radius blah', + 'no aaa group server radius blahtwo', + 'no aaa group server tacacs+ blahthree', + 'aaa group server radius blah', 'exit', + 'aaa group server radius blahtwo', 'exit']) + end + + it 'returns the resource for given name' do + expect(subject.get('blah')).to eq(blah) + end + end + + describe '#getall' do + it 'returns all of the aaa group resources' do + expect(subject.getall).to eq(all) + end + end + + describe '#create' do + it 'adds a new aaa group' do + expect(subject.create('blahthree', 'tacacs+')).to eq(true) + expect(subject.get('blahthree')).to eq(blahthree) + end + end + + describe '#delete' do + it 'removes specified aaa group' do + expect(subject.get('blahthree')).to eq(blahthree) + expect(subject.delete('blahthree')).to eq(true) + expect(subject.get('blahthree')).to eq(nil) + end + end + + describe '#set_servers' do + it 'removes all servers and then adds one' do + expect(subject.set_servers('blahtwo', [{ name: 'localhost' }])) + .to eq(true) + expect(subject.get('blahtwo')[:servers]).to eq(servers) + end + end +end diff --git a/spec/system/rbeapi/api/aaa_spec.rb b/spec/system/rbeapi/api/aaa_spec.rb new file mode 100644 index 0000000..3b37571 --- /dev/null +++ b/spec/system/rbeapi/api/aaa_spec.rb @@ -0,0 +1,90 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/aaa' + +describe Rbeapi::Api::Aaa do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + let(:test) do + { + groups: { + 'blah' => { + type: 'radius', + servers: [] + }, + 'blahtwo' => { + type: 'radius', + servers: [] + } + } + } + end + + describe '#get' do + before do + node.config(['no aaa group server radius blah', + 'no aaa group server radius blahtwo', + 'aaa group server radius blah', 'exit', + 'aaa group server radius blahtwo', 'exit']) + end + + it 'returns the resource for given name' do + expect(subject.get).to eq(test) + end + + it 'returns a hash' do + expect(subject.get).to be_a_kind_of(Hash) + end + + it 'has two entries' do + expect(subject.get[:groups].size).to eq(2) + end + end + + describe '#groups' do + it 'returns new node instance' do + expect(subject.groups).to be_a_kind_of(Rbeapi::Api::AaaGroups) + end + + it 'returns a hash' do + expect(subject.groups).to be_a_kind_of(Object) + end + end +end diff --git a/spec/unit/rbeapi/api/aaa/aaa_groups_spec.rb b/spec/unit/rbeapi/api/aaa/aaa_groups_spec.rb new file mode 100644 index 0000000..ba41189 --- /dev/null +++ b/spec/unit/rbeapi/api/aaa/aaa_groups_spec.rb @@ -0,0 +1,111 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/api/aaa' + +include FixtureHelpers + +describe Rbeapi::Api::AaaGroups do + subject { described_class.new(node) } + + let(:node) { double('node') } + + let(:all) do + { + 'blah' => { + type: 'radius', + servers: [] + } + } + end + + let(:blah) do + { + type: 'radius', + servers: [] + } + end + + let(:blahthree) do + { + type: 'tacacs+', + servers: [] + } + end + + def aaa + aaa = Fixtures[:aaa] + return aaa if aaa + fixture('aaa', format: :text, dir: File.dirname(__FILE__)) + end + + before :each do + allow(subject.node).to receive(:running_config).and_return(aaa) + end + + describe '#get' do + it 'returns the resource for given name' do + expect(subject.get('blah')).to eq(blah) + end + end + + describe '#getall' do + it 'returns all of the aaa group resources' do + expect(subject.getall).to eq(all) + end + end + + describe '#create' do + it 'adds a new aaa group' do + expect(node).to receive(:config) + .with(['aaa group server tacacs+ blahthree', 'exit']) + expect(subject.create('blahthree', 'tacacs+')).to eq(true) + end + end + + describe '#delete' do + it 'removes specified aaa group' do + expect(subject.delete('blahthree')).to eq(true) + expect(subject.get('blahthree')).to eq(nil) + end + end + + describe '#set_servers' do + it 'removes all servers and then adds one' do + expect(node).to receive(:config) + .with(['aaa group server radius blah', 'server localhost ', 'exit']) + expect(subject.set_servers('blah', [{ name: 'localhost' }])) + .to eq(true) + end + end +end diff --git a/spec/unit/rbeapi/api/aaa/aaa_spec.rb b/spec/unit/rbeapi/api/aaa/aaa_spec.rb new file mode 100644 index 0000000..090bc71 --- /dev/null +++ b/spec/unit/rbeapi/api/aaa/aaa_spec.rb @@ -0,0 +1,77 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/api/aaa' + +include FixtureHelpers + +describe Rbeapi::Api::Aaa do + subject { described_class.new(node) } + + let(:node) { double('node') } + + let(:test) do + { + groups: { + 'blah' => { + type: 'radius', + servers: [] + } + } + } + end + + def aaa + aaa = Fixtures[:aaa] + return aaa if aaa + fixture('aaa', format: :text, dir: File.dirname(__FILE__)) + end + + before :each do + allow(subject.node).to receive(:running_config).and_return(aaa) + end + + describe '#get' do + it 'returns the resource for given name' do + expect(subject.get).to eq(test) + end + + it 'returns a hash' do + expect(subject.get).to be_a_kind_of(Hash) + end + + it 'has two entries' do + expect(subject.get.size).to eq(1) + end + end +end diff --git a/spec/unit/rbeapi/api/aaa/fixture_aaa.text b/spec/unit/rbeapi/api/aaa/fixture_aaa.text new file mode 100644 index 0000000..78b8bd2 --- /dev/null +++ b/spec/unit/rbeapi/api/aaa/fixture_aaa.text @@ -0,0 +1,3 @@ +aaa group server radius blah +no aaa root +aaa authentication policy local allow-nopassword-remote-login \ No newline at end of file From f929ebbe6d95da34c0aa001e1fb591d8b57ad598 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Tue, 22 Dec 2015 09:22:20 -0700 Subject: [PATCH 16/41] Shorten config block in bgp test. --- spec/system/rbeapi/api/bgp_spec.rb | 128 ++++------------------------- 1 file changed, 14 insertions(+), 114 deletions(-) diff --git a/spec/system/rbeapi/api/bgp_spec.rb b/spec/system/rbeapi/api/bgp_spec.rb index 063c11c..d04078a 100644 --- a/spec/system/rbeapi/api/bgp_spec.rb +++ b/spec/system/rbeapi/api/bgp_spec.rb @@ -72,105 +72,29 @@ } end + let(:response) do + { + bgp_as: '1000', + router_id: nil, + shutdown: false, + maximum_paths: 1, + maximum_ecmp_paths: 128, + networks: [], + neighbors: {} + } + end + describe '#get' do before do node.config(['no router bgp 64600', 'router bgp 64600', - 'no shutdown', - 'router-id 192.168.254.1', - 'bgp convergence time 300', - 'bgp convergence slow-peer time 90', - 'no bgp confederation identifier', - 'no update wait-for-convergence', - 'no update wait-install', - 'bgp log-neighbor-changes', - 'bgp default ipv4-unicast', - 'no bgp default ipv6-unicast', - 'timers bgp 60 180', - 'distance bgp 20 200 200', - 'graceful-restart stalepath-time 300', - 'no bgp cluster-id', - 'bgp client-to-client reflection', - 'graceful-restart-helper', - 'bgp peer-mac-resolution-timeout 0', - 'bgp enforce-first-as', - 'no default-metric', - 'no bgp always-compare-med', - 'no bgp bestpath med missing-as-worst', - 'no bgp bestpath med confed', - 'no bgp host-routes fib direct-install', - 'maximum-paths 32 ecmp 32', - 'bgp listen limit 1000', - 'no bgp aspath-cmp-include-nexthop', - 'no bgp tie-break-on-age', - 'no bgp tie-break-on-router-id', - 'no bgp tie-break-on-originator-id', - 'no bgp tie-break-on-cluster-list-length', - 'neighbor eBGP_GROUP peer-group', - 'no neighbor eBGP_GROUP remote-as', - 'no neighbor eBGP_GROUP import-localpref', - 'no neighbor eBGP_GROUP export-localpref', - 'no neighbor eBGP_GROUP description', - 'no neighbor eBGP_GROUP next-hop-self', - 'no neighbor eBGP_GROUP next-hop-peer', - 'no neighbor eBGP_GROUP allowas-in', - 'no neighbor eBGP_GROUP send-community', - 'no neighbor eBGP_GROUP shutdown', - 'no neighbor eBGP_GROUP remove-private-as', - 'no neighbor eBGP_GROUP out-delay', - 'no neighbor eBGP_GROUP local-as', - 'no neighbor eBGP_GROUP weight', - 'no neighbor eBGP_GROUP transport connection-mode passive', - 'no neighbor eBGP_GROUP update-source', - 'no neighbor eBGP_GROUP dont-capability-negotiate', - 'neighbor eBGP_GROUP fall-over bfd', - 'no neighbor eBGP_GROUP local-v6-addr', - 'neighbor eBGP_GROUP soft-reconfiguration inbound', - 'no neighbor eBGP_GROUP ebgp-multihop', - 'no neighbor eBGP_GROUP route-reflector-client', - 'no neighbor eBGP_GROUP timers', - 'no neighbor eBGP_GROUP route-map in', - 'neighbor eBGP_GROUP graceful-restart-helper', - 'no neighbor eBGP_GROUP route-map out', - 'no neighbor eBGP_GROUP prefix-list in', - 'no neighbor eBGP_GROUP prefix-list out', - 'neighbor eBGP_GROUP password 7 6C/HkrwJ53+dsHumQf49Wg==', - 'no neighbor eBGP_GROUP default-originate', - 'neighbor eBGP_GROUP enforce-first-as', - 'no neighbor eBGP_GROUP metric-out', - 'neighbor eBGP_GROUP maximum-routes 12000', + 'router-id 192.168.254.1', 'maximum-paths 32 ecmp 32', 'neighbor 192.168.255.1 peer-group eBGP_GROUP', 'neighbor 192.168.255.1 remote-as 65000', 'neighbor 192.168.255.3 peer-group eBGP_GROUP', 'neighbor 192.168.255.3 remote-as 65001', 'network 192.168.254.1/32', 'network 192.168.254.2/32 route-map rmap', - 'bgp redistribute-internal', - 'aggregate-address 192.168.255.0/28 summary-only', - 'no redistribute connected', - 'no redistribute ospf match internal', - 'no redistribute ospf match external', - 'no redistribute ospf match nssa-external', - 'no redistribute ospf3 match internal', - 'no redistribute ospf3 match external', - 'no redistribute static', - 'no redistribute rip', - 'no redistribute aggregate', - 'address-family ipv4', - 'default neighbor eBGP_GROUP activate', - 'no neighbor eBGP_GROUP route-map in', - 'no neighbor eBGP_GROUP route-map out', - 'no neighbor eBGP_GROUP default-originate', - 'default neighbor 192.168.255.1 activate', - 'default neighbor 192.168.255.3 activate', - 'address-family ipv6', - 'default neighbor eBGP_GROUP activate', - 'no neighbor eBGP_GROUP route-map in', - 'no neighbor eBGP_GROUP route-map out', - 'no neighbor eBGP_GROUP prefix-list in', - 'no neighbor eBGP_GROUP prefix-list out', - 'no neighbor eBGP_GROUP default-originate', - 'default neighbor 192.168.255.1 activate', - 'default neighbor 192.168.255.3 activate']) + 'aggregate-address 192.168.255.0/28 summary-only']) end it 'returns the BGP resource' do @@ -179,18 +103,6 @@ end describe '#create' do - let(:response) do - { - bgp_as: '1000', - router_id: nil, - shutdown: false, - maximum_paths: 1, - maximum_ecmp_paths: 128, - networks: [], - neighbors: {} - } - end - before do node.config(['no router bgp 64600']) end @@ -247,18 +159,6 @@ node.config(['no router bgp 1000', 'router bgp 1000']) end - let(:response) do - { - bgp_as: '1000', - router_id: nil, - shutdown: false, - maximum_paths: 1, - maximum_ecmp_paths: 128, - networks: [], - neighbors: {} - } - end - it 'delete a BGP resource' do expect(subject.get).to eq(response) expect(subject.delete).to be_truthy From 37387e3efba3bb853a770fc396682677acfdfb27 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Tue, 22 Dec 2015 16:28:55 -0700 Subject: [PATCH 17/41] Checking in. --- lib/rbeapi/api/aaa.rb | 42 ++++++++++++++++++++++------------ lib/rbeapi/api/acl.rb | 2 +- lib/rbeapi/api/dns.rb | 2 +- lib/rbeapi/api/interfaces.rb | 2 +- lib/rbeapi/api/ipinterfaces.rb | 2 +- lib/rbeapi/api/logging.rb | 2 +- lib/rbeapi/api/mlag.rb | 2 +- lib/rbeapi/api/ntp.rb | 4 ++-- lib/rbeapi/api/ospf.rb | 2 +- lib/rbeapi/api/prefixlists.rb | 2 +- lib/rbeapi/api/radius.rb | 2 +- lib/rbeapi/api/routemaps.rb | 2 +- lib/rbeapi/api/snmp.rb | 2 +- lib/rbeapi/api/staticroutes.rb | 2 +- lib/rbeapi/api/stp.rb | 2 +- lib/rbeapi/api/switchports.rb | 2 +- lib/rbeapi/api/system.rb | 2 +- lib/rbeapi/api/tacacs.rb | 2 +- lib/rbeapi/api/users.rb | 2 +- lib/rbeapi/api/varp.rb | 2 +- lib/rbeapi/api/vlans.rb | 2 +- lib/rbeapi/api/vrrp.rb | 2 +- 22 files changed, 50 insertions(+), 36 deletions(-) diff --git a/lib/rbeapi/api/aaa.rb b/lib/rbeapi/api/aaa.rb index f6289c7..5392136 100644 --- a/lib/rbeapi/api/aaa.rb +++ b/lib/rbeapi/api/aaa.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Aaa class manages Authorization, Authentication and Accounting (AAA) @@ -47,6 +47,8 @@ def get response end + ## + # Returns an object node for working with AaaGroups class. def groups return @groups if @groups @groups = AaaGroups.new node @@ -55,20 +57,20 @@ def groups end ## - # The AaaGroups class manages the server groups on a EOS node. + # The AaaGroups class manages the server groups on an EOS node. class AaaGroups < Entity DEFAULT_RADIUS_AUTH_PORT = 1812 DEFAULT_RADIUS_ACCT_PORT = 1813 - # Regular express that parses the radius servers from the aaa group - # server radius configuration block + # Regular expression that parses the radius servers from the aaa group + # server radius configuration block. RADIUS_GROUP_SERVER = /\s{3}server [ ]([^\s]+) [ ]auth-port[ ](\d+) [ ]acct-port[ ](\d+)/x - # Regular expression that parse the tacacs servers from the aaa group - # server tacacs+ configuration block + # Regular expression that parses the tacacs servers from the aaa group + # server tacacs+ configuration block. TACACS_GROUP_SERVER = /\s{3}server [ ]([^\s]+) (?:[ ]vrf[ ](\w+))? @@ -76,20 +78,20 @@ class AaaGroups < Entity ## # get returns the aaa server group resource hash that describes the - # current configuration for the specified server group name + # current configuration for the specified server group name. # # The resource hash returned contains the following: # * type: (String) The server group type. Valid values are either - # 'tacacs' or 'radius' + # 'tacacs' or 'radius'. # * servers: (Array) The set of servers associated with the group. - # Servers are returned as either IP address or host name + # Servers are returned as either IP address or host name. # - # @param [String] :name The server group name to return f:rom the nodes + # @param [String] :name The server group name to return from the nodes # current running configuration. If the name is not configured a nil # object is returned. # # @return [nil, Hash] returns the resource hash for the - # specified name. If the name does not exist, a nil object is returned + # specified name. If the name does not exist, a nil object is returned. def get(name) block = get_block("aaa group server ([^\s]+) #{name}") return nil unless block @@ -99,6 +101,18 @@ def get(name) response end + ## + # getall returns a aaa server groups hash + # + # The resource hash returned contains the following: + # * name: (String) The server group name. + # * type: (String) The server group type. Valid values are either + # 'tacacs' or 'radius' + # * servers: (Array) The set of servers associated with the group. + # Servers are returned as either IP address or host name + # + # @return [Hash] returns the resource hashes for + # configured aaa groups. If none exist, a nil object is returned def getall cfg = config.scan(/aaa group server (?:radius|tacacs\+) (.+)$/) cfg.each_with_object({}) do |name, hsh| @@ -109,7 +123,7 @@ def getall ## # parse_type scans the specified configuration block and returns the - # server group type as either 'tacacs' or 'radius' The type value is + # server group type as either 'tacacs' or 'radius'. The type value is # expected to always be present in the config. # # @api private @@ -175,8 +189,8 @@ def parse_radius_server(config) private :parse_radius_server ## - # parse_tacacs_server scans the provide configuration block and returns - # the list of servers configured. The configuration block is expected to + # parse_tacacs_server scans the provided configuration block and returns + # the list of configured servers. The configuration block is expected to # be a tacacs configuration block. If there are no servers configured # for the group the servers value will return an empty array. # diff --git a/lib/rbeapi/api/acl.rb b/lib/rbeapi/api/acl.rb index f4a591b..8503cd3 100644 --- a/lib/rbeapi/api/acl.rb +++ b/lib/rbeapi/api/acl.rb @@ -33,7 +33,7 @@ require 'rbeapi/api' ## -# Eos is the toplevel namespace for working with Arista EOS nodes +# Rbeapi toplevel namespace module Rbeapi ## # Api is module namespace for working with the EOS command API diff --git a/lib/rbeapi/api/dns.rb b/lib/rbeapi/api/dns.rb index 787443d..9263020 100644 --- a/lib/rbeapi/api/dns.rb +++ b/lib/rbeapi/api/dns.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Dns class manages DNS settings on an EOS node. diff --git a/lib/rbeapi/api/interfaces.rb b/lib/rbeapi/api/interfaces.rb index d154219..dee136b 100644 --- a/lib/rbeapi/api/interfaces.rb +++ b/lib/rbeapi/api/interfaces.rb @@ -36,7 +36,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api module namespace + # Api is module namespace for working with the EOS command API module Api ## # The Interfaces class manages all physical and logical interfaces on an diff --git a/lib/rbeapi/api/ipinterfaces.rb b/lib/rbeapi/api/ipinterfaces.rb index 568f3ac..873c482 100644 --- a/lib/rbeapi/api/ipinterfaces.rb +++ b/lib/rbeapi/api/ipinterfaces.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api # # The Ipinterface class provides an instance for managing logical diff --git a/lib/rbeapi/api/logging.rb b/lib/rbeapi/api/logging.rb index 05e1f99..dc9816e 100644 --- a/lib/rbeapi/api/logging.rb +++ b/lib/rbeapi/api/logging.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Logging class manages logging settings on an EOS node. diff --git a/lib/rbeapi/api/mlag.rb b/lib/rbeapi/api/mlag.rb index 99779b7..a397c97 100644 --- a/lib/rbeapi/api/mlag.rb +++ b/lib/rbeapi/api/mlag.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Mlag class provides a configuration instance for working with diff --git a/lib/rbeapi/api/ntp.rb b/lib/rbeapi/api/ntp.rb index e1d756d..337a794 100644 --- a/lib/rbeapi/api/ntp.rb +++ b/lib/rbeapi/api/ntp.rb @@ -32,10 +32,10 @@ require 'rbeapi/api' ## -# Eos is the toplevel namespace for working with Arista EOS nodes +# Rbeapi toplevel namespace module Rbeapi ## - # Api is module namesapce for working with the EOS command API + # Api is module namespace for working with the EOS command API module Api ## # The Ntp class provides an instance for working with the nodes diff --git a/lib/rbeapi/api/ospf.rb b/lib/rbeapi/api/ospf.rb index 46486af..eb4c120 100644 --- a/lib/rbeapi/api/ospf.rb +++ b/lib/rbeapi/api/ospf.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Ospf class is a global class that provides an instance for working diff --git a/lib/rbeapi/api/prefixlists.rb b/lib/rbeapi/api/prefixlists.rb index 379819c..6bef554 100644 --- a/lib/rbeapi/api/prefixlists.rb +++ b/lib/rbeapi/api/prefixlists.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Prefixlists class provides a configuration instance for working diff --git a/lib/rbeapi/api/radius.rb b/lib/rbeapi/api/radius.rb index 86c1481..399ca86 100644 --- a/lib/rbeapi/api/radius.rb +++ b/lib/rbeapi/api/radius.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # Radius provides instance methods to retrieve and set radius configuration diff --git a/lib/rbeapi/api/routemaps.rb b/lib/rbeapi/api/routemaps.rb index ca6494c..fca8556 100644 --- a/lib/rbeapi/api/routemaps.rb +++ b/lib/rbeapi/api/routemaps.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Routemaps class manages routemaps. A route map is a list of rules diff --git a/lib/rbeapi/api/snmp.rb b/lib/rbeapi/api/snmp.rb index fd2ea60..309a814 100644 --- a/lib/rbeapi/api/snmp.rb +++ b/lib/rbeapi/api/snmp.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api is module namespace for working with eAPI abstractions + # Api is module namespace for working with the EOS command API module Api ## # The Snmp class provides a class implementation for working with the diff --git a/lib/rbeapi/api/staticroutes.rb b/lib/rbeapi/api/staticroutes.rb index 739aa64..1efe459 100644 --- a/lib/rbeapi/api/staticroutes.rb +++ b/lib/rbeapi/api/staticroutes.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Staticroutes class provides a configuration instance for working diff --git a/lib/rbeapi/api/stp.rb b/lib/rbeapi/api/stp.rb index 79def53..caf9926 100644 --- a/lib/rbeapi/api/stp.rb +++ b/lib/rbeapi/api/stp.rb @@ -34,7 +34,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Stp class provides a base class instance for working with diff --git a/lib/rbeapi/api/switchports.rb b/lib/rbeapi/api/switchports.rb index 373b7d5..eb92d0b 100644 --- a/lib/rbeapi/api/switchports.rb +++ b/lib/rbeapi/api/switchports.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api is module namesapce for working with the EOS command API + # Api is module namespace for working with the EOS command API module Api ## # The Switchport class provides a base class instance for working with diff --git a/lib/rbeapi/api/system.rb b/lib/rbeapi/api/system.rb index 34b0e06..45fceaa 100644 --- a/lib/rbeapi/api/system.rb +++ b/lib/rbeapi/api/system.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The System class configures the node system services such as diff --git a/lib/rbeapi/api/tacacs.rb b/lib/rbeapi/api/tacacs.rb index a955349..b3e9cf9 100644 --- a/lib/rbeapi/api/tacacs.rb +++ b/lib/rbeapi/api/tacacs.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api is module namesapce for working with the EOS command API + # Api is module namespace for working with the EOS command API module Api ## # Tacacs provides instance methods to retrieve and set tacacs configuration diff --git a/lib/rbeapi/api/users.rb b/lib/rbeapi/api/users.rb index 6d5cde9..d4cadd4 100644 --- a/lib/rbeapi/api/users.rb +++ b/lib/rbeapi/api/users.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api is module namesapce for working with the EOS command API + # Api is module namespace for working with the EOS command API module Api ## # The Users class provides configuration of local user resources for diff --git a/lib/rbeapi/api/varp.rb b/lib/rbeapi/api/varp.rb index 47d6d53..09f9ec6 100644 --- a/lib/rbeapi/api/varp.rb +++ b/lib/rbeapi/api/varp.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Varp class provides an instance for working with the global diff --git a/lib/rbeapi/api/vlans.rb b/lib/rbeapi/api/vlans.rb index 5ce3ad2..4934c05 100644 --- a/lib/rbeapi/api/vlans.rb +++ b/lib/rbeapi/api/vlans.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api is module namesapce for working with the EOS command API + # Api is module namespace for working with the EOS command API module Api ## # The Vlan class provides a class implementation for working with the diff --git a/lib/rbeapi/api/vrrp.rb b/lib/rbeapi/api/vrrp.rb index 69cf370..a8b1b1d 100644 --- a/lib/rbeapi/api/vrrp.rb +++ b/lib/rbeapi/api/vrrp.rb @@ -32,7 +32,7 @@ require 'rbeapi/api' ## -# Eos is the toplevel namespace for working with Arista EOS nodes +# Rbeapi toplevel namespace module Rbeapi ## # Api is module namespace for working with the EOS command API From 7391d9fd97304dfb7774301f77215f61e82ebc21 Mon Sep 17 00:00:00 2001 From: John Corbin Date: Wed, 23 Dec 2015 07:25:14 -0800 Subject: [PATCH 18/41] Added support for trunk groups. Fixes #38 --- lib/rbeapi/api/switchports.rb | 56 +++++++++++++++++++++ spec/system/rbeapi/api/switchports_spec.rb | 58 +++++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/lib/rbeapi/api/switchports.rb b/lib/rbeapi/api/switchports.rb index 373b7d5..3cefe6d 100644 --- a/lib/rbeapi/api/switchports.rb +++ b/lib/rbeapi/api/switchports.rb @@ -69,6 +69,7 @@ def get(name) response.merge!(parse_access_vlan(config)) response.merge!(parse_trunk_native_vlan(config)) response.merge!(parse_trunk_allowed_vlans(config)) + response.merge!(parse_trunk_groups(config)) response end @@ -76,16 +77,19 @@ def parse_mode(config) mdata = /(?<=\s{3}switchport\smode\s)(.+)$/.match(config) { mode: mdata[1] } end + private :parse_mode def parse_access_vlan(config) mdata = /(?<=access\svlan\s)(.+)$/.match(config) { access_vlan: mdata[1] } end + private :parse_access_vlan def parse_trunk_native_vlan(config) mdata = /(?<=trunk\snative\svlan\s)(.+)$/.match(config) { trunk_native_vlan: mdata[1] } end + private :parse_trunk_native_vlan def parse_trunk_allowed_vlans(config) mdata = /(?<=trunk\sallowed\svlan\s)(.+)$/.match(config) @@ -101,6 +105,14 @@ def parse_trunk_allowed_vlans(config) end { trunk_allowed_vlans: values } end + private :parse_trunk_allowed_vlans + + def parse_trunk_groups(config) + mdata = config.scan(/(?<=trunk\sgroup\s)(.+)$/) + mdata = mdata.flatten if mdata.length > 0 + { trunk_groups: mdata } + end + private :parse_trunk_groups ## # Retrieves all switchport interfaces from the running-config @@ -248,6 +260,50 @@ def set_access_vlan(name, opts = {}) cmd = command_builder('switchport access vlan', opts) configure_interface(name, cmd) end + + ## + # Configures the trunk group vlans for the specified interface. + # Trunk groups not currently set are added and trunk groups + # currently configured but not in the passed in value array are removed. + # + # @param [String] name The name of the interface to configure + # @param [Hash] opts The configuration parameters for the interface + # @option opts [string] :value Set of values to configure the trunk group + # @option opts [Boolean] :enable If false then the command is + # negated. Default is true. + # @option opts [Boolean] :default The value should be set to default + # Default takes precedence over enable. + # + # @return [Boolean] True if the commands succeed otherwise False + def set_trunk_groups(name, opts = {}) + default = opts.fetch(:default, false) + if default + return configure_interface(name, 'default switchport trunk group') + end + + enable = opts.fetch(:enable, true) + unless enable + return configure_interface(name, 'no switchport trunk group') + end + + value = opts.fetch(:value, []) + fail ArgumentError, 'value must be an Array' unless value.is_a?(Array) + + value = Set.new value + current_value = Set.new get(name)[:trunk_groups] + + cmds = [] + # Add trunk groups that are not currently in the list + value.difference(current_value).each do |group| + cmds << "switchport trunk group #{group}" + end + + # Remove trunk groups that are not in the new list + current_value.difference(value).each do |group| + cmds << "no switchport trunk group #{group}" + end + configure_interface(name, cmds) if cmds.length > 0 + end end end end diff --git a/spec/system/rbeapi/api/switchports_spec.rb b/spec/system/rbeapi/api/switchports_spec.rb index dff9316..e9658a1 100644 --- a/spec/system/rbeapi/api/switchports_spec.rb +++ b/spec/system/rbeapi/api/switchports_spec.rb @@ -13,7 +13,8 @@ describe '#get' do let(:keys) do - [:mode, :access_vlan, :trunk_native_vlan, :trunk_allowed_vlans] + [:mode, :access_vlan, :trunk_native_vlan, :trunk_allowed_vlans, + :trunk_groups] end before do @@ -206,4 +207,59 @@ expect(subject.get('Ethernet1')[:trunk_allowed_vlans].length).to eq(4094) end end + + describe '#set_trunk_groups' do + before do + node.config(['interface Ethernet1', 'default switchport trunk group']) + end + + it 'raises an ArgumentError if value is not an array' do + expect { subject.set_trunk_groups('Ethernet1', value: 'foo') } + .to raise_error(ArgumentError) + end + + it 'sets trunk group to foo bar bang' do + node.config(['interface Ethernet1', 'switchport trunk group bang', + 'switchport trunk group baz']) + expect(subject.get('Ethernet1')[:trunk_groups]).to eq(%w(bang baz)) + expect(subject.set_trunk_groups('Ethernet1', value: %w(foo bar bang))) + .to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups].sort) + .to eq(%w(bang bar foo)) + end + + it 'clears trunk group if no value specified' do + node.config(['interface Ethernet1', 'switchport trunk group bang', + 'switchport trunk group baz']) + expect(subject.get('Ethernet1')[:trunk_groups]).to eq(%w(bang baz)) + expect(subject.set_trunk_groups('Ethernet1')).to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups]).to be_empty + end + + it 'negate switchport trunk group' do + node.config(['interface Ethernet1', 'switchport trunk group bang', + 'switchport trunk group baz']) + expect(subject.get('Ethernet1')[:trunk_groups]).to eq(%w(bang baz)) + expect(subject.set_trunk_groups('Ethernet1', value: %w(foo bar bang))) + .to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups].sort) + .to eq(%w(bang bar foo)) + expect(subject.set_trunk_groups('Ethernet1', enable: false)) + .to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups]).to be_empty + end + + it 'default switchport trunk group' do + node.config(['interface Ethernet1', 'switchport trunk group bang', + 'switchport trunk group baz']) + expect(subject.get('Ethernet1')[:trunk_groups]).to eq(%w(bang baz)) + expect(subject.set_trunk_groups('Ethernet1', value: %w(foo bar bang))) + .to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups].sort) + .to eq(%w(bang bar foo)) + expect(subject.set_trunk_groups('Ethernet1', default: true)) + .to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups]).to be_empty + end + end end From c7d93f427d293ab645a2e5c41d679ff395ff5f59 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Wed, 23 Dec 2015 09:10:44 -0700 Subject: [PATCH 19/41] Shorten config block. --- spec/system/rbeapi/api/vrrp_spec.rb | 38 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/spec/system/rbeapi/api/vrrp_spec.rb b/spec/system/rbeapi/api/vrrp_spec.rb index 55f2ddc..486982e 100644 --- a/spec/system/rbeapi/api/vrrp_spec.rb +++ b/spec/system/rbeapi/api/vrrp_spec.rb @@ -20,29 +20,33 @@ describe '#get' do before do - node.config(['no interface Vlan150', 'no interface Vlan100', - 'interface Vlan100', 'interface Vlan150', - 'ip address 40.10.5.8/24', 'vrrp 30 priority 100', + node.config(['no interface Vlan150', + 'no interface Vlan100', + 'interface Vlan100', + 'interface Vlan150', + 'ip address 40.10.5.8/24', + 'vrrp 30 priority 100', 'vrrp 30 timers advertise 1', 'vrrp 30 mac-address advertisement-interval 30', - 'no vrrp 30 preempt', 'vrrp 30 preempt delay minimum 0', - 'vrrp 30 preempt delay reload 0', 'vrrp 30 delay reload 0', - 'no vrrp 30 authentication', 'vrrp 30 ip 40.10.5.31', - 'vrrp 30 ipv6 ::', 'vrrp 30 description The description', - 'vrrp 30 shutdown', 'vrrp 30 track Ethernet1 decrement 5', - 'no vrrp 30 bfd ip', 'no vrrp 30 bfd ipv6', - 'vrrp 30 ip version 2', 'vrrp 40 priority 200', + 'no vrrp 30 preempt', + 'vrrp 30 preempt delay reload 0', + 'vrrp 30 delay reload 0', + 'vrrp 30 ip 40.10.5.31', + 'vrrp 30 description The description', + 'vrrp 30 shutdown', + 'vrrp 30 track Ethernet1 decrement 5', + 'vrrp 30 ip version 2', + 'vrrp 40 priority 200', 'vrrp 40 timers advertise 1', 'vrrp 40 mac-address advertisement-interval 30', - 'vrrp 40 preempt', 'vrrp 40 preempt delay minimum 0', - 'vrrp 40 preempt delay reload 0', 'vrrp 40 delay reload 0', - 'no vrrp 40 authentication', 'vrrp 40 ip 40.10.5.32', - 'vrrp 40 ipv6 ::', 'no vrrp 40 description', - 'no vrrp 40 shutdown', + 'vrrp 40 preempt', + 'vrrp 40 preempt delay reload 0', + 'vrrp 40 delay reload 0', + 'vrrp 40 ip 40.10.5.32', 'vrrp 40 track Ethernet3 decrement 33', 'vrrp 40 track Ethernet2 decrement 22', - 'vrrp 40 track Ethernet2 shutdown', 'no vrrp 40 bfd ip', - 'no vrrp 40 bfd ipv6', 'vrrp 40 ip version 2']) + 'vrrp 40 track Ethernet2 shutdown', + 'vrrp 40 ip version 2']) end let(:entity) do From 4ab55c67319c76d5716c71dba9212372c68e7850 Mon Sep 17 00:00:00 2001 From: John Corbin Date: Fri, 1 Jan 2016 15:31:54 -0800 Subject: [PATCH 20/41] Add lacp_mode option when setting port-channel members. Fixes #58 --- lib/rbeapi/api/interfaces.rb | 29 ++++++-- .../rbeapi/api/interfaces_portchannel_spec.rb | 68 +++++++++++++++++++ 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/lib/rbeapi/api/interfaces.rb b/lib/rbeapi/api/interfaces.rb index d154219..e60f511 100644 --- a/lib/rbeapi/api/interfaces.rb +++ b/lib/rbeapi/api/interfaces.rb @@ -773,24 +773,41 @@ def set_minimum_links(name, opts = {}) # @param [Array] :members The array of physical interface members to add # to the port-channel logical interface. # + # @param [str] :mode The LACP mode to configure the member interfaces to. + # Valid values are 'on, 'passive', 'active'. When there are + # existing channel-group members and their lacp mode differs + # from this attribute, all of those members will be removed and + # then re-added using the specified lacp mode. If this attribute + # is omitted, the existing lacp mode will be used for new + # member additions. + # # @return [Boolean] returns true if the command completed successfully - def set_members(name, members) + def set_members(name, members, mode = nil) current_members = Set.new parse_members(name)[:members] members = Set.new members + lacp_mode = parse_lacp_mode(name)[:lacp_mode] + if mode && mode != lacp_mode + lacp_mode = mode + set_lacp_mode(name, lacp_mode) + end + + cmds = [] + grpid = /(\d+)/.match(name)[0] + # remove members from the current port-channel interface current_members.difference(members).each do |intf| - result = remove_member(name, intf) - return false unless result + cmds << "interface #{intf}" + cmds << "no channel-group #{grpid}" end # add new member interfaces to the port-channel members.difference(current_members).each do |intf| - result = add_member(name, intf) - return false unless result + cmds << "interface #{intf}" + cmds << "channel-group #{grpid} mode #{lacp_mode}" end - true + configure(cmds) end ## diff --git a/spec/system/rbeapi/api/interfaces_portchannel_spec.rb b/spec/system/rbeapi/api/interfaces_portchannel_spec.rb index 8afd324..6814e98 100644 --- a/spec/system/rbeapi/api/interfaces_portchannel_spec.rb +++ b/spec/system/rbeapi/api/interfaces_portchannel_spec.rb @@ -134,6 +134,74 @@ expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 Ethernet3)) end + + it 'updates the member interfaces and mode on existing interface' do + node.config(['no interface Port-Channel1', 'interface Ethernet1-2', + 'channel-group 1 mode on']) + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2)) + expect(subject.get('Port-Channel1')[:lacp_mode]).to eq('on') + expect(subject.set_members('Port-Channel1', + %w(Ethernet1 Ethernet3), + 'active')).to be_truthy + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet3)) + expect(subject.get('Port-Channel1')[:lacp_mode]).to eq('active') + end + end + + describe '#add_member' do + before do + node.config(['no interface Port-Channel1', + 'interface Port-Channel1']) + end + + it 'adds new members to the port-channel interface' do + node.config(['no interface Port-Channel1', 'interface Port-Channel1']) + expect(subject.get('Port-Channel1')[:members]).not_to include('Ethernet1') + expect(subject.add_member('Port-Channel1', 'Ethernet1')).to be_truthy + expect(subject.get('Port-Channel1')[:members]).to eq(['Ethernet1']) + end + + it 'updates the member interfaces on existing interface' do + node.config(['no interface Port-Channel1', 'interface Ethernet1-2', + 'channel-group 1 mode on']) + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2)) + expect(subject.add_member('Port-Channel1', 'Ethernet3')).to be_truthy + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2 + Ethernet3)) + expect(subject.get('Port-Channel1')[:lacp_mode]).to eq('on') + end + + it 'no update to the member interfaces on existing interface' do + node.config(['no interface Port-Channel1', 'interface Ethernet1-2', + 'channel-group 1 mode active']) + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2)) + expect(subject.add_member('Port-Channel1', 'Ethernet2')).to be_truthy + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2)) + expect(subject.get('Port-Channel1')[:lacp_mode]).to eq('active') + end + end + + describe '#remove_member' do + before do + node.config(['no interface Port-Channel1', + 'interface Port-Channel1']) + end + + it 'removes the member interface on existing interface' do + node.config(['no interface Port-Channel1', 'interface Ethernet1-2', + 'channel-group 1 mode on']) + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2)) + expect(subject.remove_member('Port-Channel1', 'Ethernet1')).to be_truthy + expect(subject.get('Port-Channel1')[:members]).to eq(['Ethernet2']) + expect(subject.get('Port-Channel1')[:lacp_mode]).to eq('on') + end end describe '#set_lacp_mode' do From c40e1bc75d9b10e731f4eab0e5b4fa625d800ab9 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Sat, 2 Jan 2016 14:35:53 -0700 Subject: [PATCH 21/41] Working on beefing up framework tests. --- spec/fixtures/test.conf | 46 +++--- spec/system/rbeapi/client_spec.rb | 78 ++++++++-- spec/unit/rbeapi/client/default_spec.rb | 180 ++++++++++++++++++------ 3 files changed, 236 insertions(+), 68 deletions(-) diff --git a/spec/fixtures/test.conf b/spec/fixtures/test.conf index e955d73..595eda7 100644 --- a/spec/fixtures/test.conf +++ b/spec/fixtures/test.conf @@ -1,29 +1,39 @@ [connection:veos01] -username : eapi -password : password -transport : http -host : veos01 +username: eapi +password: password +transport: http +host: veos01 [connection:veos02] -transport : http -host : veos02 +transport: http +host: veos02 [connection:veos03] -transport : socket -host : veos03 +transport: socket +host: veos03 [connection:veos04] -host : 172.16.10.1 -username : eapi -password : password -enablepwd : itsasecret -port : 1234 -transport : https +host: 172.16.10.1 +username: eapi +password: password +enablepwd: itsasecret +port: 1234 +transport: https + +[connection:veos05] +host: 172.16.131.40 +username: admin +password: admin +enablepwd: password +transport: https +port: 1234 +open_timeout: 12 +read_timeout: 12 [connection: localhost] -transport : http_local -host : localhost +transport: http_local +host: localhost [connection:localhost] -transport : socket -host : localhost +transport: socket +host: localhost diff --git a/spec/system/rbeapi/client_spec.rb b/spec/system/rbeapi/client_spec.rb index 078d919..83867dd 100644 --- a/spec/system/rbeapi/client_spec.rb +++ b/spec/system/rbeapi/client_spec.rb @@ -35,6 +35,34 @@ def test_conf } end + let(:veos05) do + { + 'host' => '172.16.131.40', + 'username' => 'admin', + 'password' => 'admin', + 'enablepwd' => 'password', + 'transport' => 'https', + 'port' => 1234, + 'open_timeout' => 12, + 'read_timeout' => 12 + } + end + + let(:test_data) do + [ + '[connection:veos01]', + '[connection:veos02]', + '[connection:veos03', + '[connection:veos04]', + '[connection:veos05]', + '[connection: localhost]', + 'username', + 'password', + 'transport', + 'host' + ] + end + # Client class methods describe '#connect_to' do it 'retrieves the node config' do @@ -50,10 +78,15 @@ def test_conf end describe '#config_for' do - it 'returns the configuration options for the connection' do + it 'returns the configuration options for veos01' do expect(subject.config.read(test_conf)).to eq(nil) expect(subject.config_for('veos01')).to eq(veos01) end + + it 'returns the configuration options for veos05' do + expect(subject.config.read(test_conf)).to eq(nil) + expect(subject.config_for('veos05')).to eq(veos05) + end end describe '#load_config' do @@ -67,7 +100,7 @@ def test_conf # Config class methods describe 'config' do it 'gets the loaded configuration file data' do - expect(subject.config.to_s).to eq(test) + expect(subject.config.to_s).to include(test_data[0]) end end @@ -75,14 +108,18 @@ def test_conf it 'read the specified filename and load it' do expect(subject.load_config(dut_conf)).to eq(transport: 'socket') expect(subject.config.read(test_conf)).to eq(nil) - expect(subject.config.to_s).to eq(test) + expect(subject.config.to_s).to include(test_data[0]) end end describe '#get_connection' do - it 'get connection dut' do + it 'get connection veos01' do expect(subject.config.get_connection('veos01')).to eq(veos01) end + + it 'get connection veos05' do + expect(subject.config.get_connection('veos05')).to eq(veos05) + end end describe '#reload' do @@ -91,7 +128,7 @@ def test_conf .to eq(transport: 'socket') expect(subject.config.reload(filename: [test_conf])) .to eq(nil) - expect(subject.config.to_s).to eq(test) + expect(subject.config.to_s).to include(test_data[0]) end end @@ -143,15 +180,40 @@ def test_conf describe '#enable' do it 'puts the switch into privilege mode' do + expect(node.enable('show hostname')[0]) + .to include(:command, :result, :encoding) + end + + it 'puts the switch into privilege mode with encoding' do expect(node.enable('show hostname', encoding: 'text')[0]) .to include(:command, :result, :encoding) end + + it 'puts the switch into privilege mode with open and read timeout' do + expect(node.enable('show hostname', + open_timeout: 29, + read_timeout: 29)[0]).to include(:command, + :result, + :encoding) + end end describe '#run_commands' do - it 'send commands to node' do - expect(node.run_commands('show hostname', encoding: 'text')[0]) - .to include('output') + it 'sends commands to node' do + expect(node.run_commands(['show hostname'])[0]) + .to include('fqdn', 'hostname') + end + + it 'sends commands to node with encoding' do + expect(node.run_commands('show hostname', encoding: 'text')[0]['output']) + .to include('FQDN', 'Hostname') + end + + it 'sends commands with open and read timeout' do + expect(node.run_commands('show hostname', + open_timeout: 26, + read_timeout: 26)[0]).to include('fqdn', + 'hostname') end end diff --git a/spec/unit/rbeapi/client/default_spec.rb b/spec/unit/rbeapi/client/default_spec.rb index 03aaeda..bdbd874 100644 --- a/spec/unit/rbeapi/client/default_spec.rb +++ b/spec/unit/rbeapi/client/default_spec.rb @@ -38,7 +38,10 @@ describe Rbeapi::Client do subject { described_class } - let(:node) { double('node') } + let(:node) do + subject.config.read(fixture_file('dut.conf')) + subject.connect_to('dut') + end def dut_conf fixture_file('dut.conf') @@ -65,24 +68,68 @@ def test_conf } end + let(:veos05) do + { + 'host' => '172.16.131.40', + 'username' => 'admin', + 'password' => 'admin', + 'enablepwd' => 'password', + 'transport' => 'https', + 'port' => 1234, + 'open_timeout' => 12, + 'read_timeout' => 12 + } + end + + let(:test_data) do + [ + '[connection:veos01]', + '[connection:veos02]', + '[connection:veos03', + '[connection:veos04]', + '[connection:veos05]', + '[connection: localhost]', + 'username', + 'password', + 'transport', + 'host' + ] + end + # Client class methods describe '#config_for' do it 'returns the configuration options for the connection' do expect(subject.load_config(test_conf)).to eq(nil) expect(subject.config_for('veos01')).to eq(veos01) end + + it 'returns nil if connection does not exist' do + expect(subject.config_for('veos22')).to eq(nil) + end end describe '#connect_to' do it 'retrieves the node config' do expect(subject.connect_to('veos01')).to be_truthy end + + it 'returns nil if connection does not exist' do + expect(subject.connect_to('veos22')).to eq(nil) + end end describe '#load_config' do it 'overrides the default conf file loaded in the config' do expect(subject.load_config(test_conf)).to eq(nil) + end + + it 'returns nil if connection does not exit' do + expect(subject.load_config(test_conf)).to eq(nil) expect(subject.config_for('dut')).to eq(nil) + end + + it 'returns conf settings if connection exists' do + expect(subject.load_config(test_conf)).to eq(nil) expect(subject.config_for('veos01')).to eq(veos01) end end @@ -91,22 +138,31 @@ def test_conf describe 'config' do it 'gets the loaded configuration file data' do expect(subject.load_config(test_conf)).to eq(nil) - expect(subject.config.to_s).to eq(test) + expect(subject.config.to_s).to include(test_data[0]) end end describe '#read' do - it 'read the specified filename and load it' do - expect(subject.load_config(dut_conf)).to eq(transport: 'socket') + it 'read the specified filename and load dut' do + expect(subject.config.read(dut_conf)).to eq(transport: 'socket') + expect(subject.config.to_s) + .to include('host', 'username', 'password', '[connection:dut]') + end + + it 'read the specified filename and load test' do expect(subject.config.read(test_conf)).to eq(nil) - expect(subject.config.to_s).to eq(test) + expect(subject.config.to_s).to include(test_data[0]) end end describe '#get_connection' do - it 'get connection dut' do + it 'get connection veos01' do expect(subject.config.get_connection('veos01')).to eq(veos01) end + + it 'get connection veos05' do + expect(subject.config.get_connection('veos05')).to eq(veos05) + end end describe '#reload' do @@ -141,90 +197,130 @@ def test_conf # Node Class Methods describe '#running_config' do it 'gets the nodes running config' do - allow(node).to receive(:running_config).and_return(test) - expect(node).to receive(:running_config) - expect(node.running_config.to_s).to eq(test) + expect(node.running_config).to be_truthy + end + + it 'expects running config to return a string' do + expect(node.running_config).to be_kind_of(String) end end describe '#startup_config' do it 'gets the nodes startup-configuration' do - allow(node).to receive(:startup_config).and_return(test) - expect(node).to receive(:startup_config) - expect(node.startup_config).to eq(test) + expect(node.startup_config).to be_truthy + end + + it 'expects startup-configuration to be a string' do + expect(node.startup_config).to be_kind_of(String) end end describe '#enable_authentication' do it 'gets the nodes startup-configuration' do - expect(node).to receive(:enable_authentication).with('newpassword') - expect(node.enable_authentication('newpassword')).to eq(nil) + expect(node.enable_authentication('newpassword')).to eq('newpassword') end end describe '#config' do it 'puts switch into config mode' do - expect(node).to receive(:config) - .with(['no ip virtual-router mac-address']) - expect(node.config(['no ip virtual-router mac-address'])).to eq(nil) + expect(node.config(['no ip virtual-router mac-address'])) + .to be_truthy + end + + it 'expects config to return array' do + expect(node.config(['no ip virtual-router mac-address'])) + .to be_kind_of(Array) end - it 'puts switch into config mode with options' do - expect(node).to receive(:config) - .with(['no ip virtual-router mac-address'], - encoding: 'json', - open_timeout: 27.00, - read_timeout: 27.00) + it 'puts switch into config mode with options and returns array' do expect(node.config(['no ip virtual-router mac-address'], encoding: 'json', open_timeout: 27.00, - read_timeout: 27.00)).to eq(nil) + read_timeout: 27.00)) + .to be_kind_of(Array) + end + + it 'returns error if invalid command' do + expect { node.config(['no ip virtual-router mac-addresses']) } + .to raise_error Rbeapi::Eapilib::CommandError end end describe '#enable' do it 'puts the switch into privilege mode' do - expect(node).to receive(:enable).with('show hostname', encoding: 'text') - expect(node.enable('show hostname', encoding: 'text')) - .to eq(nil) + expect(node.enable('show hostname')[0][:result]) + .to include('fqdn', 'hostname') + end + + it 'puts the switch into privilege mode with encoding' do + expect(node.enable('show hostname', encoding: 'text')[0][:encoding]) + .to eq('text') + end + + it 'puts the switch into privilege mode with strict option' do + expect(node.enable('show hostname', strict: true)[0]) + .to include(:command, :result, :encoding) + end + + it 'puts the switch into privilege mode with read and open timeout' do + expect(node.enable('show hostname', + read_timeout: 29, + open_timeout: 29)[0]) + .to include(:command, :result, :encoding) + end + + it 'raises invalid command error' do + expect { node.enable(['show hostname', 'do this thing']) } + .to raise_error Rbeapi::Eapilib::CommandError end end describe '#run_commands' do + it 'expects run_commands to be a string' do + expect(node.run_commands('show hostname', encoding: 'text')[0]['output']) + .to be_kind_of String + end + it 'send commands to node' do - expect(node).to receive(:run_commands) - .with('show hostname', encoding: 'text') - expect(node.run_commands('show hostname', encoding: 'text')) - .to eq(nil) + expect(node.run_commands('show hostname', encoding: 'text')[0]['output']) + .to include('Hostname:', 'FQDN:') + end + + it 'expects run_commands to raise a command error' do + expect { node.run_commands('do this thing') } + .to raise_error Rbeapi::Eapilib::CommandError end end describe '#get_config' do - it 'will retrieve the specified configuration' do - expect(node).to receive(:get_config) - .with(config: 'running-config') + it 'will retrieve the specified configuration and return array' do expect(node.get_config(config: 'running-config')) - .to eq(nil) + .to be_kind_of(Array) end - it 'will retrieve the specified configuration with param' do - expect(node).to receive(:get_config) - .with(config: 'running-config', param: 'all') + it 'will retrieve with param and return array' do expect(node.get_config(config: 'running-config', param: 'all')) - .to eq(nil) + .to be_kind_of(Array) + end + + it 'raises invalid command error' do + expect { node.get_config(config: 'running-configurations') } + .to raise_error Rbeapi::Eapilib::CommandError end end describe '#api' do it 'returns api module' do - expect(node).to receive(:api).with('vlans') - expect(node.api('vlans')).to eq(nil) + expect(node.api('vlans')).to be_kind_of(Rbeapi::Api::Vlans) + end + + it 'returns error if invalid name' do + expect { node.api('vlanss') }.to raise_error end end describe '#refresh' do it 'refreshes configs for next call' do - expect(node).to receive(:refresh) expect(node.refresh).to eq(nil) end end From 193d87b817c18f1f9ad7d6202640086cb7de8fb3 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Sun, 3 Jan 2016 23:01:25 -0700 Subject: [PATCH 22/41] Expanding test coverage. --- spec/unit/rbeapi/client/default_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spec/unit/rbeapi/client/default_spec.rb b/spec/unit/rbeapi/client/default_spec.rb index bdbd874..57f142d 100644 --- a/spec/unit/rbeapi/client/default_spec.rb +++ b/spec/unit/rbeapi/client/default_spec.rb @@ -240,6 +240,19 @@ def test_conf .to be_kind_of(Array) end + describe 'set dry run' do + before do + # Prevents puts from writing to console + allow($stdout).to receive(:puts) + node.dry_run = true + end + + it 'expects config to do dry run' do + expect(node.config(['no ip virtual-router mac-address'])) + .to eq(nil) + end + end + it 'returns error if invalid command' do expect { node.config(['no ip virtual-router mac-addresses']) } .to raise_error Rbeapi::Eapilib::CommandError @@ -286,6 +299,11 @@ def test_conf .to include('Hostname:', 'FQDN:') end + it 'sends commands with enablepwd set' do + expect(node.enable_authentication('icanttellyou')).to eq('icanttellyou') + expect(node.run_commands('show hostname')).to be_truthy + end + it 'expects run_commands to raise a command error' do expect { node.run_commands('do this thing') } .to raise_error Rbeapi::Eapilib::CommandError From 9508dbb767b498cbd0c2b6d5f09804a3c3e954c5 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 4 Jan 2016 10:47:18 -0700 Subject: [PATCH 23/41] Remove unit tests because node cannot be mocked for these tests. --- spec/system/rbeapi/client_spec.rb | 194 ++++++++++--- spec/unit/rbeapi/client/default_spec.rb | 345 ------------------------ 2 files changed, 160 insertions(+), 379 deletions(-) delete mode 100644 spec/unit/rbeapi/client/default_spec.rb diff --git a/spec/system/rbeapi/client_spec.rb b/spec/system/rbeapi/client_spec.rb index 83867dd..df3269d 100644 --- a/spec/system/rbeapi/client_spec.rb +++ b/spec/system/rbeapi/client_spec.rb @@ -1,3 +1,34 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# require 'spec_helper' require 'rbeapi/client' @@ -64,35 +95,39 @@ def test_conf end # Client class methods - describe '#connect_to' do - it 'retrieves the node config' do - expect(node).to be_kind_of(Rbeapi::Client::Node) + describe '#config_for' do + it 'returns the configuration options for the connection' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) end - end - describe '#config' do - it 'returns the currently loaded config object' do - expect(subject.config.read(dut_conf)).to eq(transport: 'socket') - expect(subject.connect_to('dut')).to be_kind_of(Rbeapi::Client::Node) + it 'returns nil if connection does not exist' do + expect(subject.config_for('veos22')).to eq(nil) end end - describe '#config_for' do - it 'returns the configuration options for veos01' do - expect(subject.config.read(test_conf)).to eq(nil) - expect(subject.config_for('veos01')).to eq(veos01) + describe '#connect_to' do + it 'retrieves the node config' do + expect(subject.connect_to('veos01')).to be_truthy end - it 'returns the configuration options for veos05' do - expect(subject.config.read(test_conf)).to eq(nil) - expect(subject.config_for('veos05')).to eq(veos05) + it 'returns nil if connection does not exist' do + expect(subject.connect_to('veos22')).to eq(nil) end end describe '#load_config' do it 'overrides the default conf file loaded in the config' do expect(subject.load_config(test_conf)).to eq(nil) + end + + it 'returns nil if connection does not exit' do + expect(subject.load_config(test_conf)).to eq(nil) expect(subject.config_for('dut')).to eq(nil) + end + + it 'returns conf settings if connection exists' do + expect(subject.load_config(test_conf)).to eq(nil) expect(subject.config_for('veos01')).to eq(veos01) end end @@ -100,13 +135,19 @@ def test_conf # Config class methods describe 'config' do it 'gets the loaded configuration file data' do + expect(subject.load_config(test_conf)).to eq(nil) expect(subject.config.to_s).to include(test_data[0]) end end describe '#read' do - it 'read the specified filename and load it' do - expect(subject.load_config(dut_conf)).to eq(transport: 'socket') + it 'read the specified filename and load dut' do + expect(subject.config.read(dut_conf)).to eq(transport: 'socket') + expect(subject.config.to_s) + .to include('host', 'username', 'password', '[connection:dut]') + end + + it 'read the specified filename and load test' do expect(subject.config.read(test_conf)).to eq(nil) expect(subject.config.to_s).to include(test_data[0]) end @@ -124,11 +165,11 @@ def test_conf describe '#reload' do it 'reloads the configuration file' do - expect(subject.load_config(dut_conf)) + expect(subject.config.get_connection('veos01')).to eq(veos01) + expect(subject.config.reload(filename: [dut_conf])) .to eq(transport: 'socket') - expect(subject.config.reload(filename: [test_conf])) - .to eq(nil) - expect(subject.config.to_s).to include(test_data[0]) + expect(subject.config.get_connection('veos01')).to eq(nil) + expect(subject.config.get_connection('dut')).not_to be_nil end end @@ -143,19 +184,38 @@ def test_conf password: 'test', transport: 'http', host: 'test2') + expect(subject.config.get_connection('test2')) + .to eq(username: 'test2', + password: 'test', + transport: 'http', + host: 'test2') end end # Node Class Methods + describe 'node' do + it 'retrieves the node' do + expect(node).to be_kind_of(Rbeapi::Client::Node) + end + end + describe '#running_config' do it 'gets the nodes running config' do - expect(node.running_config).not_to be_nil + expect(node.running_config).to be_truthy + end + + it 'expects running config to return a string' do + expect(node.running_config).to be_kind_of(String) end end describe '#startup_config' do it 'gets the nodes startup-configuration' do - expect(node.startup_config).not_to be_nil + expect(node.startup_config).to be_truthy + end + + it 'expects startup-configuration to be a string' do + expect(node.startup_config).to be_kind_of(String) end end @@ -167,46 +227,81 @@ def test_conf describe '#config' do it 'puts switch into config mode' do - expect(node.config(['no ip virtual-router mac-address'])).to be_truthy + expect(node.config(['no ip virtual-router mac-address'])) + .to be_truthy end - it 'puts switch into config mode with options' do + it 'expects config to return array' do + expect(node.config(['no ip virtual-router mac-address'])) + .to be_kind_of(Array) + end + + it 'puts switch into config mode with options and returns array' do expect(node.config(['no ip virtual-router mac-address'], encoding: 'json', open_timeout: 27.00, - read_timeout: 27.00)).to be_truthy + read_timeout: 27.00)) + .to be_kind_of(Array) + end + + describe 'set dry run' do + before do + # Prevents puts from writing to console + allow($stdout).to receive(:puts) + node.dry_run = true + end + + it 'expects config to do dry run' do + expect(node.config(['no ip virtual-router mac-address'])) + .to eq(nil) + end + end + + it 'returns error if invalid command' do + expect { node.config(['no ip virtual-router mac-addresses']) } + .to raise_error Rbeapi::Eapilib::CommandError end end describe '#enable' do it 'puts the switch into privilege mode' do - expect(node.enable('show hostname')[0]) - .to include(:command, :result, :encoding) + expect(node.enable('show hostname')[0][:result]) + .to include('fqdn', 'hostname') end it 'puts the switch into privilege mode with encoding' do - expect(node.enable('show hostname', encoding: 'text')[0]) + expect(node.enable('show hostname', encoding: 'text')[0][:encoding]) + .to eq('text') + end + + it 'puts the switch into privilege mode with strict option' do + expect(node.enable('show hostname', strict: true)[0]) .to include(:command, :result, :encoding) end - it 'puts the switch into privilege mode with open and read timeout' do + it 'puts the switch into privilege mode with read and open timeout' do expect(node.enable('show hostname', open_timeout: 29, read_timeout: 29)[0]).to include(:command, :result, :encoding) end + + it 'raises invalid command error' do + expect { node.enable(['show hostname', 'do this thing']) } + .to raise_error Rbeapi::Eapilib::CommandError + end end describe '#run_commands' do - it 'sends commands to node' do - expect(node.run_commands(['show hostname'])[0]) - .to include('fqdn', 'hostname') + it 'expects run_commands to be a string' do + expect(node.run_commands('show hostname', encoding: 'text')[0]['output']) + .to be_kind_of String end it 'sends commands to node with encoding' do expect(node.run_commands('show hostname', encoding: 'text')[0]['output']) - .to include('FQDN', 'Hostname') + .to include('FQDN:', 'Hostname:') end it 'sends commands with open and read timeout' do @@ -215,12 +310,43 @@ def test_conf read_timeout: 26)[0]).to include('fqdn', 'hostname') end + + it 'sends commands with enablepwd set' do + expect(node.enable_authentication('icanttellyou')).to eq('icanttellyou') + expect(node.run_commands('show hostname')).to be_truthy + end + + it 'expects run_commands to raise a command error' do + expect { node.run_commands('do this thing') } + .to raise_error Rbeapi::Eapilib::CommandError + end + end + + describe '#get_config' do + it 'will retrieve the specified configuration and return array' do + expect(node.get_config(config: 'running-config')) + .to be_kind_of(Array) + end + + it 'will retrieve with param and return array' do + expect(node.get_config(config: 'running-config', param: 'all')) + .to be_kind_of(Array) + end + + it 'raises invalid command error' do + expect { node.get_config(config: 'running-configurations') } + .to raise_error Rbeapi::Eapilib::CommandError + end end describe '#api' do it 'returns api module' do expect(node.api('vlans')).to be_kind_of(Rbeapi::Api::Vlans) end + + it 'returns error if invalid name' do + expect { node.api('vlanss') }.to raise_error + end end describe '#refresh' do diff --git a/spec/unit/rbeapi/client/default_spec.rb b/spec/unit/rbeapi/client/default_spec.rb deleted file mode 100644 index 57f142d..0000000 --- a/spec/unit/rbeapi/client/default_spec.rb +++ /dev/null @@ -1,345 +0,0 @@ -# -# Copyright (c) 2015, Arista Networks, Inc. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# Neither the name of Arista Networks nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS -# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR -# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE -# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -require 'spec_helper' - -require 'rbeapi/client' - -include FixtureHelpers - -describe Rbeapi::Client do - subject { described_class } - - let(:node) do - subject.config.read(fixture_file('dut.conf')) - subject.connect_to('dut') - end - - def dut_conf - fixture_file('dut.conf') - end - - def test_conf - fixture_file('test.conf') - end - - let(:dut) do - File.read(dut_conf) - end - - let(:test) do - File.read(test_conf) - end - - let(:veos01) do - { - 'username' => 'eapi', - 'password' => 'password', - 'transport' => 'http', - 'host' => 'veos01' - } - end - - let(:veos05) do - { - 'host' => '172.16.131.40', - 'username' => 'admin', - 'password' => 'admin', - 'enablepwd' => 'password', - 'transport' => 'https', - 'port' => 1234, - 'open_timeout' => 12, - 'read_timeout' => 12 - } - end - - let(:test_data) do - [ - '[connection:veos01]', - '[connection:veos02]', - '[connection:veos03', - '[connection:veos04]', - '[connection:veos05]', - '[connection: localhost]', - 'username', - 'password', - 'transport', - 'host' - ] - end - - # Client class methods - describe '#config_for' do - it 'returns the configuration options for the connection' do - expect(subject.load_config(test_conf)).to eq(nil) - expect(subject.config_for('veos01')).to eq(veos01) - end - - it 'returns nil if connection does not exist' do - expect(subject.config_for('veos22')).to eq(nil) - end - end - - describe '#connect_to' do - it 'retrieves the node config' do - expect(subject.connect_to('veos01')).to be_truthy - end - - it 'returns nil if connection does not exist' do - expect(subject.connect_to('veos22')).to eq(nil) - end - end - - describe '#load_config' do - it 'overrides the default conf file loaded in the config' do - expect(subject.load_config(test_conf)).to eq(nil) - end - - it 'returns nil if connection does not exit' do - expect(subject.load_config(test_conf)).to eq(nil) - expect(subject.config_for('dut')).to eq(nil) - end - - it 'returns conf settings if connection exists' do - expect(subject.load_config(test_conf)).to eq(nil) - expect(subject.config_for('veos01')).to eq(veos01) - end - end - - # Config class methods - describe 'config' do - it 'gets the loaded configuration file data' do - expect(subject.load_config(test_conf)).to eq(nil) - expect(subject.config.to_s).to include(test_data[0]) - end - end - - describe '#read' do - it 'read the specified filename and load dut' do - expect(subject.config.read(dut_conf)).to eq(transport: 'socket') - expect(subject.config.to_s) - .to include('host', 'username', 'password', '[connection:dut]') - end - - it 'read the specified filename and load test' do - expect(subject.config.read(test_conf)).to eq(nil) - expect(subject.config.to_s).to include(test_data[0]) - end - end - - describe '#get_connection' do - it 'get connection veos01' do - expect(subject.config.get_connection('veos01')).to eq(veos01) - end - - it 'get connection veos05' do - expect(subject.config.get_connection('veos05')).to eq(veos05) - end - end - - describe '#reload' do - it 'reloads the configuration file' do - expect(subject.config.get_connection('veos01')).to eq(veos01) - expect(subject.config.reload(filename: [dut_conf])) - .to eq(transport: 'socket') - expect(subject.config.get_connection('veos01')).to eq(nil) - expect(subject.config.get_connection('dut')).not_to be_nil - end - end - - describe '#add_connection' do - it 'adds a new connection section' do - expect(subject.config.add_connection('test2', - username: 'test2', - password: 'test', - transport: 'http', - host: 'test2' - )).to eq(username: 'test2', - password: 'test', - transport: 'http', - host: 'test2') - expect(subject.config.get_connection('test2')) - .to eq(username: 'test2', - password: 'test', - transport: 'http', - host: 'test2') - end - end - - # Node Class Methods - describe '#running_config' do - it 'gets the nodes running config' do - expect(node.running_config).to be_truthy - end - - it 'expects running config to return a string' do - expect(node.running_config).to be_kind_of(String) - end - end - - describe '#startup_config' do - it 'gets the nodes startup-configuration' do - expect(node.startup_config).to be_truthy - end - - it 'expects startup-configuration to be a string' do - expect(node.startup_config).to be_kind_of(String) - end - end - - describe '#enable_authentication' do - it 'gets the nodes startup-configuration' do - expect(node.enable_authentication('newpassword')).to eq('newpassword') - end - end - - describe '#config' do - it 'puts switch into config mode' do - expect(node.config(['no ip virtual-router mac-address'])) - .to be_truthy - end - - it 'expects config to return array' do - expect(node.config(['no ip virtual-router mac-address'])) - .to be_kind_of(Array) - end - - it 'puts switch into config mode with options and returns array' do - expect(node.config(['no ip virtual-router mac-address'], - encoding: 'json', - open_timeout: 27.00, - read_timeout: 27.00)) - .to be_kind_of(Array) - end - - describe 'set dry run' do - before do - # Prevents puts from writing to console - allow($stdout).to receive(:puts) - node.dry_run = true - end - - it 'expects config to do dry run' do - expect(node.config(['no ip virtual-router mac-address'])) - .to eq(nil) - end - end - - it 'returns error if invalid command' do - expect { node.config(['no ip virtual-router mac-addresses']) } - .to raise_error Rbeapi::Eapilib::CommandError - end - end - - describe '#enable' do - it 'puts the switch into privilege mode' do - expect(node.enable('show hostname')[0][:result]) - .to include('fqdn', 'hostname') - end - - it 'puts the switch into privilege mode with encoding' do - expect(node.enable('show hostname', encoding: 'text')[0][:encoding]) - .to eq('text') - end - - it 'puts the switch into privilege mode with strict option' do - expect(node.enable('show hostname', strict: true)[0]) - .to include(:command, :result, :encoding) - end - - it 'puts the switch into privilege mode with read and open timeout' do - expect(node.enable('show hostname', - read_timeout: 29, - open_timeout: 29)[0]) - .to include(:command, :result, :encoding) - end - - it 'raises invalid command error' do - expect { node.enable(['show hostname', 'do this thing']) } - .to raise_error Rbeapi::Eapilib::CommandError - end - end - - describe '#run_commands' do - it 'expects run_commands to be a string' do - expect(node.run_commands('show hostname', encoding: 'text')[0]['output']) - .to be_kind_of String - end - - it 'send commands to node' do - expect(node.run_commands('show hostname', encoding: 'text')[0]['output']) - .to include('Hostname:', 'FQDN:') - end - - it 'sends commands with enablepwd set' do - expect(node.enable_authentication('icanttellyou')).to eq('icanttellyou') - expect(node.run_commands('show hostname')).to be_truthy - end - - it 'expects run_commands to raise a command error' do - expect { node.run_commands('do this thing') } - .to raise_error Rbeapi::Eapilib::CommandError - end - end - - describe '#get_config' do - it 'will retrieve the specified configuration and return array' do - expect(node.get_config(config: 'running-config')) - .to be_kind_of(Array) - end - - it 'will retrieve with param and return array' do - expect(node.get_config(config: 'running-config', param: 'all')) - .to be_kind_of(Array) - end - - it 'raises invalid command error' do - expect { node.get_config(config: 'running-configurations') } - .to raise_error Rbeapi::Eapilib::CommandError - end - end - - describe '#api' do - it 'returns api module' do - expect(node.api('vlans')).to be_kind_of(Rbeapi::Api::Vlans) - end - - it 'returns error if invalid name' do - expect { node.api('vlanss') }.to raise_error - end - end - - describe '#refresh' do - it 'refreshes configs for next call' do - expect(node.refresh).to eq(nil) - end - end -end From 49538e73d9ebd402f48caec6a422ab6f2577dedf Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 4 Jan 2016 12:55:26 -0700 Subject: [PATCH 24/41] Add unit tests back. --- spec/unit/rbeapi/client_spec.rb | 168 ++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 spec/unit/rbeapi/client_spec.rb diff --git a/spec/unit/rbeapi/client_spec.rb b/spec/unit/rbeapi/client_spec.rb new file mode 100644 index 0000000..57af804 --- /dev/null +++ b/spec/unit/rbeapi/client_spec.rb @@ -0,0 +1,168 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/client' + +include FixtureHelpers + +describe Rbeapi::Client do + subject { described_class } + + let(:node) { double('node') } + + def dut_conf + fixture_file('dut.conf') + end + + def test_conf + fixture_file('test.conf') + end + + let(:dut) do + File.read(dut_conf) + end + + let(:test) do + File.read(test_conf) + end + + let(:veos01) do + { + 'username' => 'eapi', + 'password' => 'password', + 'transport' => 'http', + 'host' => 'veos01' + } + end + + let(:veos05) do + { + 'host' => '172.16.131.40', + 'username' => 'admin', + 'password' => 'admin', + 'enablepwd' => 'password', + 'transport' => 'https', + 'port' => 1234, + 'open_timeout' => 12, + 'read_timeout' => 12 + } + end + + let(:test_data) do + [ + '[connection:veos01]', + '[connection:veos02]', + '[connection:veos03', + '[connection:veos04]', + '[connection:veos05]', + '[connection: localhost]', + 'username', + 'password', + 'transport', + 'host' + ] + end + + # Client class methods + describe '#config_for' do + it 'returns the configuration options for the connection' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) + end + end + + describe '#connect_to' do + it 'retrieves the node config' do + expect(subject.connect_to('veos01')).to be_truthy + end + end + + describe '#load_config' do + it 'overrides the default conf file loaded in the config' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('dut')).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) + end + end + + # Config class methods + describe 'config' do + it 'gets the loaded configuration file data' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config.to_s).to include(test_data[0]) + end + end + + describe '#read' do + it 'read the specified filename and load it' do + expect(subject.load_config(dut_conf)).to eq(transport: 'socket') + expect(subject.config.read(test_conf)).to eq(nil) + expect(subject.config.to_s).to include(test_data[0]) + end + end + + describe '#get_connection' do + it 'get connection dut' do + expect(subject.config.get_connection('veos01')).to eq(veos01) + end + end + + describe '#reload' do + it 'reloads the configuration file' do + expect(subject.config.get_connection('veos01')).to eq(veos01) + expect(subject.config.reload(filename: [dut_conf])) + .to eq(transport: 'socket') + expect(subject.config.get_connection('veos01')).to eq(nil) + expect(subject.config.get_connection('dut')).not_to be_nil + end + end + + describe '#add_connection' do + it 'adds a new connection section' do + expect(subject.config.add_connection('test2', + username: 'test2', + password: 'test', + transport: 'http', + host: 'test2' + )).to eq(username: 'test2', + password: 'test', + transport: 'http', + host: 'test2') + expect(subject.config.get_connection('test2')) + .to eq(username: 'test2', + password: 'test', + transport: 'http', + host: 'test2') + end + end +end From 52c2b394862034d6b2cf8d5add2c64140cd93345 Mon Sep 17 00:00:00 2001 From: John Corbin Date: Tue, 5 Jan 2016 06:27:26 -0800 Subject: [PATCH 25/41] Relax check on getall entries Routemaps that exist on the switch before test was run will be returned by getall. There is no easy way to clear out all the routemaps and didn't want to loop on the entries not created by test and delete them. --- spec/system/rbeapi/api/routemaps_spec.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/system/rbeapi/api/routemaps_spec.rb b/spec/system/rbeapi/api/routemaps_spec.rb index 4b6edca..a40b456 100644 --- a/spec/system/rbeapi/api/routemaps_spec.rb +++ b/spec/system/rbeapi/api/routemaps_spec.rb @@ -15,8 +15,7 @@ let(:resource) { subject.get } before do - node.config(['no route-map test', 'no route-map test1', - 'no route-map test2', 'no route-map test3', + node.config(['no route-map test', 'route-map test permit 10', 'route-map test permit 20', 'description descript', 'match ip address prefix-list MYLOOPBACK', @@ -90,8 +89,8 @@ expect(subject.getall).to be_a_kind_of(Hash) end - it 'has a key for description' do - expect(subject.getall.count).to eq(2) + it 'has at least two entries' do + expect(subject.getall.count).to be > 2 end it 'returns the routemap collection' do From 244e122e4f3774c29d89222a9af27db54e84adcc Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Wed, 6 Jan 2016 21:02:25 -0700 Subject: [PATCH 26/41] test timeout values --- lib/rbeapi/eapilib.rb | 8 ++++++++ spec/system/rbeapi/client_spec.rb | 19 +++++++++++++++++++ spec/unit/rbeapi/client_spec.rb | 2 -- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/rbeapi/eapilib.rb b/lib/rbeapi/eapilib.rb index c16bce2..9472e67 100644 --- a/lib/rbeapi/eapilib.rb +++ b/lib/rbeapi/eapilib.rb @@ -162,6 +162,14 @@ def timeouts(opts = {}) @read_timeout = opts.fetch(:read_timeout, DEFAULT_HTTP_READ_TIMEOUT) end + ## + # Gets values for open_timeout and read_timeout + # + # @return [Hash] open_timeout and read_timeout + def get_timeouts + { open_timeout: @open_timeout, read_timeout: @read_timeout } + end + ## # Generates the eAPI JSON request message. # diff --git a/spec/system/rbeapi/client_spec.rb b/spec/system/rbeapi/client_spec.rb index df3269d..b422caf 100644 --- a/spec/system/rbeapi/client_spec.rb +++ b/spec/system/rbeapi/client_spec.rb @@ -354,4 +354,23 @@ def test_conf expect(node.refresh).to eq(nil) end end + + describe 'test timeouts' do + it 'loads default timeout values' do + expect(node.connection.get_timeouts).to eq(open_timeout: 10, + read_timeout: 10) + end + + describe 'loads veos05' do + let(:node) do + subject.config.read(fixture_file('test.conf')) + subject.connect_to('veos05') + end + + it 'loads timeout values from conf file' do + expect(node.connection.get_timeouts).to eq(open_timeout: 12, + read_timeout: 12) + end + end + end end diff --git a/spec/unit/rbeapi/client_spec.rb b/spec/unit/rbeapi/client_spec.rb index 57af804..2e52615 100644 --- a/spec/unit/rbeapi/client_spec.rb +++ b/spec/unit/rbeapi/client_spec.rb @@ -38,8 +38,6 @@ describe Rbeapi::Client do subject { described_class } - let(:node) { double('node') } - def dut_conf fixture_file('dut.conf') end From 9c93a32febb52d4da2d1eb4fe72c0db3130384c2 Mon Sep 17 00:00:00 2001 From: John Corbin Date: Wed, 6 Jan 2016 20:29:47 -0800 Subject: [PATCH 27/41] Test check should be at least 2, not more than 2. --- spec/system/rbeapi/api/routemaps_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/system/rbeapi/api/routemaps_spec.rb b/spec/system/rbeapi/api/routemaps_spec.rb index a40b456..ee3cff4 100644 --- a/spec/system/rbeapi/api/routemaps_spec.rb +++ b/spec/system/rbeapi/api/routemaps_spec.rb @@ -90,7 +90,7 @@ end it 'has at least two entries' do - expect(subject.getall.count).to be > 2 + expect(subject.getall.count).to be >= 2 end it 'returns the routemap collection' do From eb7721924aabf7cef1987615348b82300da64995 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Thu, 7 Jan 2016 11:19:45 -0700 Subject: [PATCH 28/41] Get initial unit tests running. --- .../rbeapi/api/switchports/default_spec.rb | 245 +++++++++++++++ .../api/switchports/fixture_switchports.text | 284 ++++++++++++++++++ 2 files changed, 529 insertions(+) create mode 100644 spec/unit/rbeapi/api/switchports/default_spec.rb create mode 100644 spec/unit/rbeapi/api/switchports/fixture_switchports.text diff --git a/spec/unit/rbeapi/api/switchports/default_spec.rb b/spec/unit/rbeapi/api/switchports/default_spec.rb new file mode 100644 index 0000000..d34dccf --- /dev/null +++ b/spec/unit/rbeapi/api/switchports/default_spec.rb @@ -0,0 +1,245 @@ +# +# Copyright (c) 2015, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/api/switchports' + +include FixtureHelpers + +describe Rbeapi::Api::Switchports do + subject { described_class.new(node) } + + let(:node) { double('node') } + + def switchports + switchports = Fixtures[:switchports] + return switchports if switchports + fixture('switchports', format: :text, dir: File.dirname(__FILE__)) + end + + before :each do + allow(subject.node).to receive(:running_config).and_return(switchports) + end + + describe '#get' do + let(:keys) do + [:mode, :access_vlan, :trunk_native_vlan, :trunk_allowed_vlans, + :trunk_groups] + end + + context 'vlan as an integer range' do + it 'returns the switchport resource' do + expect(subject.get('Ethernet1')).not_to be_nil + end + + it 'does not return a nonswitchport resource' do + expect(subject.get('Ethernet2')).to be_nil + end + + it 'has all required keys' do + expect(subject.get('Ethernet1').keys).to eq(keys) + end + + it 'returns allowed_vlans as an array' do + expect(subject.get('Ethernet1')[:trunk_allowed_vlans]) + .to be_a_kind_of(Array) + end + end + + context 'vlan as an integer' do + it 'returns the switchport resource' do + expect(subject.get('Ethernet1')).not_to be_nil + end + end + end + + describe '#getall' do + it 'returns the switchport collection' do + expect(subject.getall).to include('Ethernet1') + end + + it 'returns a hash collection' do + expect(subject.getall).to be_a_kind_of(Hash) + end + end + + describe '#create' do + it 'creates a new switchport resource' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no ip address', 'switchport']) + expect(subject.create('Ethernet1')).to be_truthy + end + end + + describe '#delete' do + it 'deletes a switchport resource' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport']) + expect(subject.delete('Ethernet1')).to be_truthy + end + end + + describe '#default' do + it 'sets Ethernet1 to default' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport']) + expect(subject.default('Ethernet1')).to be_truthy + end + end + + describe '#set_mode' do + it 'sets mode value to access' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport mode access']) + expect(subject.set_mode('Ethernet1', value: 'access')).to be_truthy + end + + it 'sets the mode value to trunk' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport mode trunk']) + expect(subject.set_mode('Ethernet1', value: 'trunk')).to be_truthy + end + + it 'negate the mode value' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport mode']) + expect(subject.set_mode('Ethernet1', enable: false)).to be_truthy + end + + it 'default the mode value' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport mode']) + expect(subject.set_mode('Ethernet1', default: true)).to be_truthy + end + end + + describe '#set_access_vlan' do + it 'sets the access vlan value to 100' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport access vlan 100']) + expect(subject.set_access_vlan('Ethernet1', value: '100')).to be_truthy + end + + it 'negates the access vlan value' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport access vlan']) + expect(subject.set_access_vlan('Ethernet1', enable: false)).to be_truthy + end + + it 'defaults the access vlan value' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport access vlan']) + expect(subject.set_access_vlan('Ethernet1', default: true)).to be_truthy + end + end + + describe '#set_trunk_native_vlan' do + it 'sets the trunk native vlan to 100' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport trunk native vlan 100']) + expect(subject.set_trunk_native_vlan('Ethernet1', value: '100')) + .to be_truthy + end + + it 'negates the trunk native vlan' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport trunk native vlan']) + expect(subject.set_trunk_native_vlan('Ethernet1', enable: false)) + .to be_truthy + end + + it 'defaults the trunk native vlan' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport trunk native vlan']) + expect(subject.set_trunk_native_vlan('Ethernet1', default: true)) + .to be_truthy + end + end + + describe '#set_trunk_allowed_vlans' do + it 'raises an ArgumentError if value is not an array' do + expect { subject.set_trunk_allowed_vlans('Ethernet1', value: '1-100') } + .to raise_error(ArgumentError) + end + + it 'sets vlan 8 and 9 to the trunk allowed vlans' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport trunk allowed vlan none', + 'switchport trunk allowed vlan 8,9']) + expect(subject.set_trunk_allowed_vlans('Ethernet1', value: [8, 9])) + .to be_truthy + end + + it 'negate switchport trunk allowed vlan' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport trunk allowed vlan']) + expect(subject.set_trunk_allowed_vlans('Ethernet1', enable: false)) + .to be_truthy + end + + it 'default switchport trunk allowed vlan' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport trunk allowed vlan']) + expect(subject.set_trunk_allowed_vlans('Ethernet1', default: true)) + .to be_truthy + end + end + + describe '#set_trunk_groups' do + it 'raises an ArgumentError if value is not an array' do + expect { subject.set_trunk_groups('Ethernet1', value: 'foo') } + .to raise_error(ArgumentError) + end + + it 'sets trunk group to foo bar bang' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport trunk group foo', + 'switchport trunk group bar', 'switchport trunk group bang']) + expect(subject.set_trunk_groups('Ethernet1', value: %w(foo bar bang))) + .to be_truthy + end + + it 'negate switchport trunk group' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport trunk group']) + expect(subject.set_trunk_groups('Ethernet1', enable: false)) + .to be_truthy + end + + it 'default switchport trunk group' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport trunk group']) + expect(subject.set_trunk_groups('Ethernet1', default: true)) + .to be_truthy + end + end +end diff --git a/spec/unit/rbeapi/api/switchports/fixture_switchports.text b/spec/unit/rbeapi/api/switchports/fixture_switchports.text new file mode 100644 index 0000000..060b80c --- /dev/null +++ b/spec/unit/rbeapi/api/switchports/fixture_switchports.text @@ -0,0 +1,284 @@ +interface Ethernet1 + no description + no shutdown + default load-interval + logging event link-status use-global + no dcbx mode + no mac-address + no link-debounce + no flowcontrol send + no flowcontrol receive + no mac timestamp + no speed + no l2 mtu + default logging event congestion-drops + default unidirectional + switchport access vlan 1 + switchport trunk native vlan 1 + switchport trunk allowed vlan 1-4094 + switchport mode access + switchport mac address learning + no switchport private-vlan mapping + switchport + default encapsulation dot1q vlan + no l2-protocol encapsulation dot1q vlan 0 + snmp trap link-status + no channel-group + lacp rate normal + lacp port-priority 32768 + lldp transmit + lldp receive + no msrp + no mvrp + no switchport port-security + switchport port-security maximum 1 + default qos trust + qos cos 5 + qos dscp 2 + no shape rate + mc-tx-queue 0 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 1 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 2 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 3 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 0 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 1 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 2 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 3 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 4 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 5 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 6 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 7 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + sflow enable + no spanning-tree portfast + spanning-tree portfast auto + no spanning-tree link-type + no spanning-tree bpduguard + no spanning-tree bpdufilter + no spanning-tree cost + spanning-tree port-priority 128 + no spanning-tree guard + no spanning-tree bpduguard rate-limit + logging event spanning-tree use-global + switchport tap native vlan 1 + no switchport tap identity + switchport tap allowed vlan 1-4094 + switchport tool allowed vlan 1-4094 + no switchport tool identity + no switchport tap truncation + no switchport tool truncation + no switchport tap default group + no switchport tool group + no switchport tool dot1q remove outer +! +interface Ethernet2 + no description + no shutdown + default load-interval + mtu 1500 + logging event link-status use-global + no dcbx mode + no mac-address + no link-debounce + no flowcontrol send + no flowcontrol receive + no mac timestamp + no speed + no l2 mtu + default logging event congestion-drops + default unidirectional + no switchport + default encapsulation dot1q vlan + no l2-protocol encapsulation dot1q vlan 0 + snmp trap link-status + no ip proxy-arp + no ip local-proxy-arp + no ip address + no ip verify unicast + default arp timeout 14400 + default ipv6 nd cache expire 14400 + bfd interval 300 min_rx 300 multiplier 3 + no bfd echo + default ip dhcp smart-relay + no ip helper-address + no ipv6 dhcp relay destination + ip dhcp relay information option circuit-id Ethernet2 + no ip igmp + ip igmp version 3 + ip igmp last-member-query-count 2 + ip igmp last-member-query-interval 10 + ip igmp query-max-response-time 100 + ip igmp query-interval 125 + ip igmp startup-query-count 2 + ip igmp startup-query-interval 310 + ip igmp router-alert optional connected + ip igmp host-proxy + no ip igmp host-proxy report-interval + ip igmp host-proxy version 3 + no ip igmp host-proxy + no ipv6 enable + no ipv6 address + no ipv6 verify unicast + no ipv6 nd ra suppress + ipv6 nd ra interval msec 200000 + ipv6 nd ra lifetime 1800 + no ipv6 nd ra mtu suppress + no ipv6 nd managed-config-flag + no ipv6 nd other-config-flag + ipv6 nd reachable-time 0 + ipv6 nd router-preference medium + ipv6 nd ra dns-servers lifetime 300 + ipv6 nd ra dns-suffixes lifetime 300 + ipv6 nd ra hop-limit 64 + no channel-group + lacp rate normal + lacp port-priority 32768 + lldp transmit + lldp receive + ip mfib fastdrop + no msrp + no mvrp + default ntp serve + no ip pim sparse-mode + no ip pim border-router + ip pim query-interval 30 + ip pim join-prune-interval 60 + ip pim dr-priority 1 + no ip pim neighbor-filter + default ip pim bfd-instance + no ip pim bsr-border + default qos trust + qos cos 5 + qos dscp 2 + no shape rate + mc-tx-queue 0 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 1 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 2 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + mc-tx-queue 3 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 0 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 1 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 2 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 3 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 4 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 5 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 6 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + ! + uc-tx-queue 7 + priority strict + no bandwidth percent + no shape rate + no bandwidth guaranteed + sflow enable +! From 71aaa8d1a968ba89148f1bba69bfdc69328cd22c Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Thu, 7 Jan 2016 17:01:47 -0700 Subject: [PATCH 29/41] Ensure all parse methods are private. --- lib/rbeapi/api/dns.rb | 3 +++ lib/rbeapi/api/logging.rb | 1 + lib/rbeapi/api/ntp.rb | 1 + lib/rbeapi/api/system.rb | 2 ++ 4 files changed, 7 insertions(+) diff --git a/lib/rbeapi/api/dns.rb b/lib/rbeapi/api/dns.rb index 787443d..835eee3 100644 --- a/lib/rbeapi/api/dns.rb +++ b/lib/rbeapi/api/dns.rb @@ -64,17 +64,20 @@ def parse_domain_name mdata = /ip domain-name ([\w.]+)/.match(config) { domain_name: mdata.nil? ? '' : mdata[1] } end + private :parse_domain_name def parse_name_servers servers = config.scan(/(?:ip name-server vrf )(?:\w+)\s(.+)/) values = servers.each_with_object([]) { |srv, arry| arry << srv.first } { name_servers: values } end + private :parse_name_servers def parse_domain_list search = config.scan(/(?<=^ip\sdomain-list\s).+$/) { domain_list: search } end + private :parse_domain_list ## # Configure the domain-name value in the running-config diff --git a/lib/rbeapi/api/logging.rb b/lib/rbeapi/api/logging.rb index 05e1f99..9f1908e 100644 --- a/lib/rbeapi/api/logging.rb +++ b/lib/rbeapi/api/logging.rb @@ -80,6 +80,7 @@ def parse_hosts hosts = config.scan(/(?<=^logging\shost\s)[^\s]+/) { hosts: hosts } end + private :parse_hosts ## # set_enable configures the global logging instance on the node as either diff --git a/lib/rbeapi/api/ntp.rb b/lib/rbeapi/api/ntp.rb index e1d756d..11df1ba 100644 --- a/lib/rbeapi/api/ntp.rb +++ b/lib/rbeapi/api/ntp.rb @@ -95,6 +95,7 @@ def parse_servers end { servers: values } end + private :parse_servers ## # set_source_interface configures the ntp source value in the nodes diff --git a/lib/rbeapi/api/system.rb b/lib/rbeapi/api/system.rb index 34b0e06..1b4487e 100644 --- a/lib/rbeapi/api/system.rb +++ b/lib/rbeapi/api/system.rb @@ -62,11 +62,13 @@ def parse_hostname(config) mdata = /(?<=^hostname\s)(.+)$/.match(config) { hostname: mdata.nil? ? '' : mdata[1] } end + private :parse_hostname def parse_iprouting(config) mdata = /no\sip\srouting/.match(config) { iprouting: mdata.nil? ? true : false } end + private :parse_iprouting ## # Configures the system hostname value in the running-config From a295697e4004a14d98b0a33470fc7b3f8c277a05 Mon Sep 17 00:00:00 2001 From: John Corbin Date: Fri, 8 Jan 2016 10:06:54 -0800 Subject: [PATCH 30/41] Catch errors and syslog them when parsing eapi conf file. Created utils method to syslog warning messages. Updated add_connection to return nil instead of returning connection values. Updated tests to deal with add_connection return value change. Added test case for bad conf file and empty conf file. Added test case to verify EAPI_CONF path is used. Fixes issue #82 --- lib/rbeapi/client.rb | 17 ++++++---- lib/rbeapi/utils.rb | 10 ++++++ spec/fixtures/eapi.conf.yaml | 6 ++++ spec/fixtures/empty.conf | 0 spec/fixtures/env_path.conf | 5 +++ spec/system/rbeapi/client_spec.rb | 32 +++---------------- spec/unit/rbeapi/client_spec.rb | 53 +++++++++++++++++++++---------- 7 files changed, 71 insertions(+), 52 deletions(-) create mode 100644 spec/fixtures/eapi.conf.yaml create mode 100644 spec/fixtures/empty.conf create mode 100644 spec/fixtures/env_path.conf diff --git a/lib/rbeapi/client.rb b/lib/rbeapi/client.rb index 640c0dd..4395a87 100644 --- a/lib/rbeapi/client.rb +++ b/lib/rbeapi/client.rb @@ -90,7 +90,7 @@ def config_for(name) end ## - # Retrieves the node config form the loaded configuration file and + # Retrieves the node config from the loaded configuration file and # returns a Rbeapi::Node instance for working with the remote node. # # @param [String] :name The named configuration to use for creating the @@ -110,10 +110,7 @@ def connect_to(name) ## # Builds a connection object to a remote node using the specified # options and return an instance of Rbeapi::Connection. All - # configuration options can be passed via the :opts param or can be - # overridden using environment variables. Environment variables are - # specified by prepending EAPI to the option name. For instance to - # override the host param use EAPI_HOST. + # configuration options can be passed via the :opts param. # # @param [Hash] :opts the options to create a message with # @option :opts [String] :host The IP address or hostname of the remote @@ -205,7 +202,12 @@ def autoload(opts = {}) # # @param [String] :filename The full path to the filename to load def read(filename) - super(filename: filename) + begin + super(filename: filename) + rescue IniFile::Error => exc + Rbeapi::Utils.syslog_warning("#{exc}: in eapi conf file: #{filename}") + return + end # For each section, if the host parameter is omitted then the # connection name is used @@ -252,13 +254,14 @@ def get_connection(name) end ## - # Adds a new connection section to the current configuration + # Adds a new connection section to the current configuration # # @param [String] :name The name of the connection to add to the # configuration. # @param [Hash] :values The properties for the connection def add_connection(name, values) self["connection:#{name}"] = values + nil end end diff --git a/lib/rbeapi/utils.rb b/lib/rbeapi/utils.rb index 348e05b..d5337ab 100644 --- a/lib/rbeapi/utils.rb +++ b/lib/rbeapi/utils.rb @@ -30,6 +30,8 @@ # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # +require 'syslog' + ## # Rbeapi toplevel namespace module Rbeapi @@ -64,5 +66,13 @@ def self.class_from_string(name) mod.const_get(cls) end end + + ## + # Syslogs a warning message. + # + # @param [String] :message The message to log. + def self.syslog_warning(message) + Syslog.open('rbeapi', Syslog::LOG_PID) { |s| s.warning message } + end end end diff --git a/spec/fixtures/eapi.conf.yaml b/spec/fixtures/eapi.conf.yaml new file mode 100644 index 0000000..9102805 --- /dev/null +++ b/spec/fixtures/eapi.conf.yaml @@ -0,0 +1,6 @@ +--- +:username: admin +:password: admin +:use_ssl: true +:port: 199 +:hostname: bogus diff --git a/spec/fixtures/empty.conf b/spec/fixtures/empty.conf new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/env_path.conf b/spec/fixtures/env_path.conf new file mode 100644 index 0000000..d5a5d12 --- /dev/null +++ b/spec/fixtures/env_path.conf @@ -0,0 +1,5 @@ +[connection:env_path] +host: 172.16.131.40 +username: admin +password: admin +transport: https diff --git a/spec/system/rbeapi/client_spec.rb b/spec/system/rbeapi/client_spec.rb index df3269d..80ec084 100644 --- a/spec/system/rbeapi/client_spec.rb +++ b/spec/system/rbeapi/client_spec.rb @@ -81,16 +81,7 @@ def test_conf let(:test_data) do [ - '[connection:veos01]', - '[connection:veos02]', - '[connection:veos03', - '[connection:veos04]', - '[connection:veos05]', - '[connection: localhost]', - 'username', - 'password', - 'transport', - 'host' + '[connection:veos01]' ] end @@ -142,7 +133,7 @@ def test_conf describe '#read' do it 'read the specified filename and load dut' do - expect(subject.config.read(dut_conf)).to eq(transport: 'socket') + expect(subject.config.read(dut_conf)).to eq(nil) expect(subject.config.to_s) .to include('host', 'username', 'password', '[connection:dut]') end @@ -166,8 +157,7 @@ def test_conf describe '#reload' do it 'reloads the configuration file' do expect(subject.config.get_connection('veos01')).to eq(veos01) - expect(subject.config.reload(filename: [dut_conf])) - .to eq(transport: 'socket') + expect(subject.config.reload(filename: [dut_conf])).to eq(nil) expect(subject.config.get_connection('veos01')).to eq(nil) expect(subject.config.get_connection('dut')).not_to be_nil end @@ -180,10 +170,7 @@ def test_conf password: 'test', transport: 'http', host: 'test2' - )).to eq(username: 'test2', - password: 'test', - transport: 'http', - host: 'test2') + )).to eq(nil) expect(subject.config.get_connection('test2')) .to eq(username: 'test2', password: 'test', @@ -219,12 +206,6 @@ def test_conf end end - describe '#enable_authentication' do - it 'gets the nodes startup-configuration' do - expect(node.enable_authentication('newpassword')).to eq('newpassword') - end - end - describe '#config' do it 'puts switch into config mode' do expect(node.config(['no ip virtual-router mac-address'])) @@ -311,11 +292,6 @@ def test_conf 'hostname') end - it 'sends commands with enablepwd set' do - expect(node.enable_authentication('icanttellyou')).to eq('icanttellyou') - expect(node.run_commands('show hostname')).to be_truthy - end - it 'expects run_commands to raise a command error' do expect { node.run_commands('do this thing') } .to raise_error Rbeapi::Eapilib::CommandError diff --git a/spec/unit/rbeapi/client_spec.rb b/spec/unit/rbeapi/client_spec.rb index 57af804..ab99da1 100644 --- a/spec/unit/rbeapi/client_spec.rb +++ b/spec/unit/rbeapi/client_spec.rb @@ -48,6 +48,14 @@ def test_conf fixture_file('test.conf') end + def empty_conf + fixture_file('empty.conf') + end + + def yaml_conf + fixture_file('eapi.conf.yaml') + end + let(:dut) do File.read(dut_conf) end @@ -80,21 +88,26 @@ def test_conf let(:test_data) do [ - '[connection:veos01]', - '[connection:veos02]', - '[connection:veos03', - '[connection:veos04]', - '[connection:veos05]', - '[connection: localhost]', - 'username', - 'password', - 'transport', - 'host' + '[connection:veos01]' ] end + let(:default_entry) { "[connection:localhost]\ntransport : socket\n" } + # Client class methods describe '#config_for' do + # Verify that the EAPI_CONF env variable path is used by default + # when the Config class is instantiated/reload-ed. + it 'env path to config file' do + # Store env path for the eapi conf file and reload the class + conf = fixture_dir + '/env_path.conf' + ENV.store('EAPI_CONF', conf) + subject.config.reload + + # Verify env_path.conf file was loaded + expect(subject.config.to_s).to include('[connection:env_path]') + end + it 'returns the configuration options for the connection' do expect(subject.load_config(test_conf)).to eq(nil) expect(subject.config_for('veos01')).to eq(veos01) @@ -121,11 +134,21 @@ def test_conf expect(subject.load_config(test_conf)).to eq(nil) expect(subject.config.to_s).to include(test_data[0]) end + + it 'loading empty config file does not fail' do + expect(subject.load_config(empty_conf)).to eq(nil) + expect(subject.config.to_s).to eq(default_entry) + end + + it 'does not load bad config file data' do + expect(subject.load_config(yaml_conf)).to eq(nil) + expect(subject.config.to_s).to eq('') + end end describe '#read' do it 'read the specified filename and load it' do - expect(subject.load_config(dut_conf)).to eq(transport: 'socket') + expect(subject.load_config(dut_conf)).to eq(nil) expect(subject.config.read(test_conf)).to eq(nil) expect(subject.config.to_s).to include(test_data[0]) end @@ -140,8 +163,7 @@ def test_conf describe '#reload' do it 'reloads the configuration file' do expect(subject.config.get_connection('veos01')).to eq(veos01) - expect(subject.config.reload(filename: [dut_conf])) - .to eq(transport: 'socket') + expect(subject.config.reload(filename: [dut_conf])).to eq(nil) expect(subject.config.get_connection('veos01')).to eq(nil) expect(subject.config.get_connection('dut')).not_to be_nil end @@ -154,10 +176,7 @@ def test_conf password: 'test', transport: 'http', host: 'test2' - )).to eq(username: 'test2', - password: 'test', - transport: 'http', - host: 'test2') + )).to eq(nil) expect(subject.config.get_connection('test2')) .to eq(username: 'test2', password: 'test', From 9cf4f68eb621b0c2e830ca7878a5fdbf7d663402 Mon Sep 17 00:00:00 2001 From: John Corbin Date: Fri, 8 Jan 2016 10:19:25 -0800 Subject: [PATCH 31/41] Set enable password for a connection. Fixes issue #72 --- lib/rbeapi/client.rb | 6 +++++- spec/system/rbeapi/client_spec.rb | 20 +++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/rbeapi/client.rb b/lib/rbeapi/client.rb index 640c0dd..2e3fe98 100644 --- a/lib/rbeapi/client.rb +++ b/lib/rbeapi/client.rb @@ -105,6 +105,10 @@ def connect_to(name) config = Rbeapi::Utils.transform_keys_to_symbols(config) connection = connect config Node.new(connection) + node = Node.new(connection) + enablepwd = config.fetch(:enablepwd, nil) + node.enable_authentication(enablepwd) if enablepwd + node end ## @@ -122,7 +126,7 @@ def connect_to(name) # the eAPI connection with # @option :opts [String] :password The password to use to authenticate # the eAPI connection with - # @option :opts [String] :enable_pwd The enable password (if defined) to + # @option :opts [String] :enablepwd The enable password (if defined) to # pass to the remote node to enter privilege mode # @option :opts [String] :use_ssl Specifies whether or not to use the # HTTP or HTTPS protocol diff --git a/spec/system/rbeapi/client_spec.rb b/spec/system/rbeapi/client_spec.rb index df3269d..db562f6 100644 --- a/spec/system/rbeapi/client_spec.rb +++ b/spec/system/rbeapi/client_spec.rb @@ -57,6 +57,8 @@ def test_conf File.read(test_conf) end + let(:enablepwd) { 'enable_admin' } + let(:veos01) do { 'username' => 'eapi', @@ -311,17 +313,25 @@ def test_conf 'hostname') end - it 'sends commands with enablepwd set' do - expect(node.enable_authentication('icanttellyou')).to eq('icanttellyou') - expect(node.run_commands('show hostname')).to be_truthy - end - it 'expects run_commands to raise a command error' do expect { node.run_commands('do this thing') } .to raise_error Rbeapi::Eapilib::CommandError end end + describe '#run_commands with enable password' do + # Before the tests Set the enable password on the dut + before(:each) { node.config(["enable secret 0 #{enablepwd}"]) } + + # After the tests clear the enable password on the dut + after(:each) { node.config(['no enable secret']) } + + it 'sends commands with enablepwd set' do + expect(node.enable_authentication(enablepwd)).to eq(enablepwd) + expect(node.run_commands('show hostname')).to be_truthy + end + end + describe '#get_config' do it 'will retrieve the specified configuration and return array' do expect(node.get_config(config: 'running-config')) From 2b40fa0dc739eb861274a1dde9f1011f6e9274bd Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Sat, 9 Jan 2016 11:45:07 -0700 Subject: [PATCH 32/41] Update get and getall documentation. --- lib/rbeapi/api/aaa.rb | 50 ++++++++++++----- lib/rbeapi/api/acl.rb | 60 ++++++++++++++++++++- lib/rbeapi/api/bgp.rb | 81 ++++++++++++++++++++++++++++ lib/rbeapi/api/dns.rb | 19 +++++++ lib/rbeapi/api/interfaces.rb | 98 +++++++++++++++++++++++++--------- lib/rbeapi/api/ipinterfaces.rb | 13 ++++- lib/rbeapi/api/logging.rb | 10 +++- lib/rbeapi/api/mlag.rb | 28 ++++++---- lib/rbeapi/api/ntp.rb | 2 +- lib/rbeapi/api/ospf.rb | 31 +++++++---- lib/rbeapi/api/prefixlists.rb | 22 ++++++-- lib/rbeapi/api/radius.rb | 16 +++--- lib/rbeapi/api/routemaps.rb | 10 ++-- lib/rbeapi/api/snmp.rb | 6 +-- lib/rbeapi/api/stp.rb | 51 +++++++++++++----- lib/rbeapi/api/switchports.rb | 70 +++++++++++++++++++++++- lib/rbeapi/api/system.rb | 23 +++++++- lib/rbeapi/api/tacacs.rb | 17 +++--- lib/rbeapi/api/users.rb | 11 +++- lib/rbeapi/api/varp.rb | 46 +++++++++++++--- lib/rbeapi/api/vlans.rb | 18 +++++-- 21 files changed, 566 insertions(+), 116 deletions(-) diff --git a/lib/rbeapi/api/aaa.rb b/lib/rbeapi/api/aaa.rb index 5392136..16dd75b 100644 --- a/lib/rbeapi/api/aaa.rb +++ b/lib/rbeapi/api/aaa.rb @@ -41,6 +41,25 @@ module Api # The Aaa class manages Authorization, Authentication and Accounting (AAA) # on an EOS node. class Aaa < Entity + ## + # get returns a hash of all Aaa resources + # + # @example + # { + # : { + # : { + # type: , + # servers: + # }, + # : { + # type: , + # servers: + # } + # } + # } + # + # @return [Hash] Returns the Aaa resources as a + # Hash. If no Aaa resources are found, an empty hash is returned. def get response = {} response[:groups] = groups.getall @@ -80,18 +99,18 @@ class AaaGroups < Entity # get returns the aaa server group resource hash that describes the # current configuration for the specified server group name. # - # The resource hash returned contains the following: - # * type: (String) The server group type. Valid values are either - # 'tacacs' or 'radius'. - # * servers: (Array) The set of servers associated with the group. - # Servers are returned as either IP address or host name. + # @example + # { + # type: , + # servers: + # } # # @param [String] :name The server group name to return from the nodes # current running configuration. If the name is not configured a nil # object is returned. # # @return [nil, Hash] returns the resource hash for the - # specified name. If the name does not exist, a nil object is returned. + # specified name. If the name does not exist, a nil object is returned def get(name) block = get_block("aaa group server ([^\s]+) #{name}") return nil unless block @@ -104,12 +123,17 @@ def get(name) ## # getall returns a aaa server groups hash # - # The resource hash returned contains the following: - # * name: (String) The server group name. - # * type: (String) The server group type. Valid values are either - # 'tacacs' or 'radius' - # * servers: (Array) The set of servers associated with the group. - # Servers are returned as either IP address or host name + # @example + # { + # : { + # type: , + # servers: + # }, + # : { + # type: , + # servers: + # } + # } # # @return [Hash] returns the resource hashes for # configured aaa groups. If none exist, a nil object is returned @@ -174,7 +198,6 @@ def parse_servers(config, type) # @param [String] :config The aaa server group block configuration for the # group name to parse # - # # @return [Hash] resource hash attribute def parse_radius_server(config) values = config.scan(RADIUS_GROUP_SERVER).map do |(name, auth, acct)| @@ -199,7 +222,6 @@ def parse_radius_server(config) # @param [String] :config The aaa server group block configuration for the # group name to parse # - # # @return [Hash] resource hash attribute def parse_tacacs_server(config) values = config.scan(TACACS_GROUP_SERVER).map do |(name, vrf, port)| diff --git a/lib/rbeapi/api/acl.rb b/lib/rbeapi/api/acl.rb index 8503cd3..dafd5f7 100644 --- a/lib/rbeapi/api/acl.rb +++ b/lib/rbeapi/api/acl.rb @@ -56,10 +56,29 @@ def initialize(node) ## # get returns the specified ACL from the nodes current configuration. # + # @example + # { + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # ... + # } + # # @param [String] :name The ACL name. # # @return [nil, Hash] Returns the ACL resource as a - # Hash. + # Hash. Returns nil if name does not exist. def get(name) config = get_block("ip access-list standard #{name}") return nil unless config @@ -72,6 +91,45 @@ def get(name) # configuration as a hash. The ACL resource collection hash is # keyed by the ACL name. # + # @example + # { + # : { + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # ... + # }, + # : { + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # ... + # }, + # ... + # } + # # @return [nil, Hash] Returns a hash that represents # the entire ACL collection from the nodes running configuration. # If there are no ACLs configured, this method will return an diff --git a/lib/rbeapi/api/bgp.rb b/lib/rbeapi/api/bgp.rb index 079e897..67d3a63 100644 --- a/lib/rbeapi/api/bgp.rb +++ b/lib/rbeapi/api/bgp.rb @@ -51,6 +51,50 @@ def initialize(node) # get returns the BGP routing configuration from the nodes current # configuration. # + # @example + # { + # bgp_as: , + # router_id: , + # shutdown: , + # maximum_paths: , + # maximum_ecmp_paths: + # networks: [ + # { + # prefix: , + # masklen: , + # route_map: + # }, + # { + # prefix: , + # masklen: , + # route_map: + # } + # ], + # neighbors: { + # name: { + # peer_group: , + # remote_as: , + # send_community: , + # shutdown: , + # description: , + # next_hop_selp: , + # route_map_in: , + # route_map_out: + # }, + # name: { + # peer_group: , + # remote_as: , + # send_community: , + # shutdown: , + # description: , + # next_hop_selp: , + # route_map_in: , + # route_map_out: + # }, + # ... + # } + # } + # # @return [nil, Hash] Returns the BGP resource as a # Hash. def get @@ -397,6 +441,18 @@ class BgpNeighbors < Entity # get returns a single BGP neighbor entry from the nodes current # configuration. # + # @example + # { + # peer_group: , + # remote_as: , + # send_community: , + # shutdown: , + # description: + # next_hop_self: + # route_map_in: + # route_map_out: + # } + # # @param [String] :name The name of the BGP neighbor to manage. # This value can be either an IPv4 address or string (in the # case of managing a peer group). @@ -422,6 +478,31 @@ def get(name) # getall returns the collection of all neighbor entries for the # BGP router instance. # + # @example + # { + # : { + # peer_group: , + # remote_as: , + # send_community: , + # shutdown: , + # description: + # next_hop_self: + # route_map_in: + # route_map_out: + # }, + # : { + # peer_group: , + # remote_as: , + # send_community: , + # shutdown: , + # description: + # next_hop_self: + # route_map_in: + # route_map_out: + # }, + # ... + # } + # # @return [nil, Hash] Returns a hash that # represents the entire BGP neighbor collection from the nodes # running configuration. If there a BGP router is not configured diff --git a/lib/rbeapi/api/dns.rb b/lib/rbeapi/api/dns.rb index 9263020..2f95494 100644 --- a/lib/rbeapi/api/dns.rb +++ b/lib/rbeapi/api/dns.rb @@ -60,17 +60,36 @@ def get response end + ## + # parse_domain_name parses the domain-name from config + # + # @api private + # + # @return [Hash] resource hash attribute def parse_domain_name mdata = /ip domain-name ([\w.]+)/.match(config) { domain_name: mdata.nil? ? '' : mdata[1] } end + ## + # parse_name_servers parses the name-server values from + # config + # + # @api private + # + # @return [Hash] resource hash attribute def parse_name_servers servers = config.scan(/(?:ip name-server vrf )(?:\w+)\s(.+)/) values = servers.each_with_object([]) { |srv, arry| arry << srv.first } { name_servers: values } end + ## + # parse_domain_list parses the domain-list from config + # + # @api private + # + # @return [Hash] resource hash attribute def parse_domain_list search = config.scan(/(?<=^ip\sdomain-list\s).+$/) { domain_list: search } diff --git a/lib/rbeapi/api/interfaces.rb b/lib/rbeapi/api/interfaces.rb index a4617bc..f2e2de3 100644 --- a/lib/rbeapi/api/interfaces.rb +++ b/lib/rbeapi/api/interfaces.rb @@ -47,10 +47,52 @@ def initialize(node) @instances = {} end + ## + # get returns a hash of interface configurations for the given name + # + # @example + # { + # name: , + # type: , + # description: , + # shutdown: + # } + # + # @param [String] :name The interface name to return a resource for from + # the nodes configuration + # + # @return [nil, Hash] Returns the interface resource as a + # Hash. If the specified name is not found in the nodes current + # configuration a nil object is returned def get(name) get_instance(name).get(name) end + ## + # getall returns a hash of interface configurations + # + # @example + # { + # : { + # name: , + # type: , + # description: , + # shutdown: , + # ... + # }, + # : { + # name: , + # type: , + # description: , + # shutdown: , + # ... + # }, + # ... + # } + # + # @return [Hash] Returns the interface resources as a + # Hash. If none exist in the nodes current + # configuration an empty hash is returned def getall interfaces = config.scan(/(?<=^interface\s).+$/) @@ -60,6 +102,13 @@ def getall end end + ## + # get_instance returns an interface instance for the given name + # + # @param [String] :name The interface name to return an instance for + # + # @return [Object] Returns the interface instance as an + # Object. def get_instance(name) name = name[0, 2].upcase case name @@ -287,20 +336,18 @@ class EthernetInterface < BaseInterface # get returns the specified Ethernet interface resource hash that # represents the interface's current configuration in the node. # - # The resource hash returned contains the following information: - # - # * name (string): the interface name (eg Ethernet1) - # * type (string): will always be 'ethernet' - # * description (string): the interface description value - # * speed (string): the current speed setting for the interface speed - # * forced (boolean): true if auto negotiation is disabled otherwise - # false - # * sflow (boolean): true if sflow is enabled on the interface - # otherwise false - # * flowcontrol_send (string): the interface flowcontrol send value. - # Valid values are 'on' or 'off' - # * flowconrol_receive (string): the interface flowcontrol receive - # value. Valid values are 'on' or 'off' + # @example + # { + # name: , + # type: , + # description: , + # shutdown: , + # speed: , + # forced: , + # sflow: , + # flowcontrol_send: , + # flowcontrol_receive: + # } # # @param [String] :name The interface name to return a resource hash # for from the node's running configuration @@ -319,7 +366,6 @@ def get(name) response.merge!(parse_sflow(config)) response.merge!(parse_flowcontrol_send(config)) response.merge!(parse_flowcontrol_receive(config)) - response end @@ -955,16 +1001,18 @@ class VxlanInterface < BaseInterface # BaseInterface get method and adds the Vxlan specific attributes to # the hash # - # The returned resource hash contains the following - # - # * name: (String) The full interface name identifier - # * type: (String) 'vxlan' - # * description: (String) The configured interface description - # * shutdown: (Boolean) The admin state of the interface - # * source_interface: (String) The vxlan source-interface value - # * multicast_group: (String) The vxlan multicast-group value - # * udp_port: (Fixnum) The vxlan udp-port value - # * flood_list: (Array) The list of VTEPs to flood traffic towards + # @example + # { + # name: , + # type: , + # description: , + # shutdown: , + # source_interface: , + # multicast_group: , + # udp_port: , + # flood_list: , + # vlans: + # } # # @param [String] :name The interface name to return from the nodes # configuration. This optional parameter defaults to Vxlan1 diff --git a/lib/rbeapi/api/ipinterfaces.rb b/lib/rbeapi/api/ipinterfaces.rb index 873c482..ddeb80c 100644 --- a/lib/rbeapi/api/ipinterfaces.rb +++ b/lib/rbeapi/api/ipinterfaces.rb @@ -79,7 +79,17 @@ def get(name) # # @example # { - # : {...} + # : { + # address: + # mtu: + # helper_addresses: array + # }, + # : { + # address: + # mtu: + # helper_addresses: array + # }, + # ... # } # # @see get Ipaddress resource example @@ -103,6 +113,7 @@ def getall # ipaddress resource hash. # # @api private + # # @param [String] :config The IP interface configuration block returned # from the node's running configuration # diff --git a/lib/rbeapi/api/logging.rb b/lib/rbeapi/api/logging.rb index dc9816e..8d2458c 100644 --- a/lib/rbeapi/api/logging.rb +++ b/lib/rbeapi/api/logging.rb @@ -46,7 +46,7 @@ class Logging < Entity # # @example # { - # enable: [true, false] + # enable: [true, false], # hosts: array # } # @@ -65,6 +65,10 @@ def get # command is expected to always be in the node's configuration. This # methods return value is intended to be merged into the logging resource # hash. + # + # @api private + # + # @return [Hash] resource hash attribute def parse_enable value = /no logging on/ !~ config { enable: value } @@ -76,6 +80,10 @@ def parse_enable # logging hosts are configured, then the value for hosts will be an empty # array. The return value is intended to be merged into the logging # resource hash + # + # @api private + # + # @return [Hash] resource hash attribute def parse_hosts hosts = config.scan(/(?<=^logging\shost\s)[^\s]+/) { hosts: hosts } diff --git a/lib/rbeapi/api/mlag.rb b/lib/rbeapi/api/mlag.rb index a397c97..e88a67f 100644 --- a/lib/rbeapi/api/mlag.rb +++ b/lib/rbeapi/api/mlag.rb @@ -50,15 +50,25 @@ class Mlag < Entity # get scans the current nodes configuration and returns the values as # a Hash describing the current state. # - # The resource hash returned contains the following: - # * domain_id: (String) The MLAG domain-id value - # * local_interface: (String) The MLAG local-interface value - # * peer_address: (String) The IP address of the MLAG peer - # * peer_link: (String) The MLAG transit peer-link value - # * shutdown: (Boolean) The administrative staet of the mlag - # configuration - # * interfaces: (Hash) The list of configured MLAG interfaces. (See - # parse_interfaces for the Hash details) + # @example + # { + # global: { + # domain_id: , + # local_interface: , + # peer_address: , + # peer_link: , + # shutdown: + # }, + # interfaces: { + # : { + # mlag_id: + # }, + # : { + # mlag_id: + # }, + # ... + # } + # } # # @see parse_interfaces # diff --git a/lib/rbeapi/api/ntp.rb b/lib/rbeapi/api/ntp.rb index 337a794..e1f2b68 100644 --- a/lib/rbeapi/api/ntp.rb +++ b/lib/rbeapi/api/ntp.rb @@ -48,7 +48,7 @@ class Ntp < Entity # # @example # { - # source_interface: + # source_interface: , # servers: { # prefer: [true, false] # } diff --git a/lib/rbeapi/api/ospf.rb b/lib/rbeapi/api/ospf.rb index eb4c120..8772825 100644 --- a/lib/rbeapi/api/ospf.rb +++ b/lib/rbeapi/api/ospf.rb @@ -48,13 +48,15 @@ class Ospf < Entity # # @example # { - # "router_id": - # "areas": { + # router_id: + # areas: { # : array # }, - # "redistribute" + # redistribute: {} # } # + # @param [String] :inst The ospf instance name + # # @return [Hash] A Ruby hash object that provides the OSPF settings as # key / value pairs. def get(inst) @@ -90,9 +92,16 @@ def get(inst) # # @example # { - # : {...} - # "interfaces": {...} + # : { + # router_id: , + # areas: {}, + # redistribute: {} + # }, + # interfaces: {} # } + # + # @return [Hash] A Ruby hash object that provides the OSPF settings as + # key / value pairs. def getall instances = config.scan(/(?<=^router\sospf\s)\d+$/) response = instances.each_with_object({}) do |inst, hsh| @@ -147,8 +156,7 @@ class OspfInterfaces < Entity # # Example # { - # "name": , - # "network_type": + # network_type: # } # # @param [String] :name The interface name to return the configuration @@ -174,8 +182,13 @@ def get(name) # # Example # { - # : {...}, - # : {...} + # : { + # network_type: + # }, + # : { + # network_type: + # }, + # ... # } # # @return [nil, Hash] A Ruby hash that represents the diff --git a/lib/rbeapi/api/prefixlists.rb b/lib/rbeapi/api/prefixlists.rb index 6bef554..1c42c31 100644 --- a/lib/rbeapi/api/prefixlists.rb +++ b/lib/rbeapi/api/prefixlists.rb @@ -48,16 +48,17 @@ class Prefixlists < Entity # @example # { # : { - # "next_hop": , - # "name": + # next_hop: , + # name: # } # } # + # @param [String] :name The name of the prefix-list to return + # # @returns [Hash The method will return all of the # configured static routes on the node as a Ruby hash object. If # there are no static routes configured, this method will return # an empty hash - def get(name) config = get_block("ip prefix-list #{name}") return nil unless config @@ -69,6 +70,21 @@ def get(name) end end + ## + # Returns the static routes configured on the node + # + # @example + # { + # : { + # next_hop: , + # name: + # } + # } + # + # @returns [Hash The method will return all of the + # configured static routes on the node as a Ruby hash object. If + # there are no static routes configured, this method will return + # an empty hash def getall lists = config.scan(/(?<=^ip\sprefix-list\s).+/) lists.each_with_object({}) do |name, hsh| diff --git a/lib/rbeapi/api/radius.rb b/lib/rbeapi/api/radius.rb index 399ca86..33b449a 100644 --- a/lib/rbeapi/api/radius.rb +++ b/lib/rbeapi/api/radius.rb @@ -61,14 +61,14 @@ class Radius < Entity # This method is intended to be used by a provider's instances class # method. # - # The resource hash returned contains the following information: - # * key: (String) the key either in plain text or hashed format - # * key_format: (Fixnum) e.g. 0 or 7 - # * timeout: (Fixnum) seconds before the timeout period ends - # * retransmit: (Fixnum), e.g. 3, attempts after first timeout expiry. - # * servers: (Array), - # - # @api public + # @example + # { + # key: , + # key_format: , + # timeout: , + # retransmit: , + # servers: + # } # # @return [Array] Single element Array of resource hashes def get diff --git a/lib/rbeapi/api/routemaps.rb b/lib/rbeapi/api/routemaps.rb index fca8556..7be95b3 100644 --- a/lib/rbeapi/api/routemaps.rb +++ b/lib/rbeapi/api/routemaps.rb @@ -81,8 +81,8 @@ class Routemaps < Entity # } # } # - # @param [String] name The routemap name to return a resource for from the - # nodes configuration + # @param [String] :name The routemap name to return a resource for from + # the nodes configuration # # @return [nil, Hash] Returns the routemap resource as a # Hash. If the specified name is not found in the nodes current @@ -174,10 +174,11 @@ def getall ## # parse entries is a private method to get the routemap rules. # + # @api private + # # @return [nil, Hash] returns a hash that represents the # rules for routemaps from the nodes running configuration. If # there are no routemaps configured, this method will return nil. - # def parse_entries(name) entries = config.scan(/^route-map\s#{name}\s.+$/) return nil if entries.empty? @@ -196,11 +197,12 @@ def parse_entries(name) ## # parse rule is a private method to parse a rule. # + # @api private + # # @return [Hash] returns a hash that represents the # rules for routemaps from the nodes running configuration. If # there are no routemaps configured, this method will return an empty # hash. - # def parse_rules(rules) rules.split("\n").each_with_object({}) do |rule, rule_hsh| mdata = /\s{3}(\w+)\s/.match(rule) diff --git a/lib/rbeapi/api/snmp.rb b/lib/rbeapi/api/snmp.rb index 309a814..6f24e44 100644 --- a/lib/rbeapi/api/snmp.rb +++ b/lib/rbeapi/api/snmp.rb @@ -57,9 +57,9 @@ class Snmp < Entity # # @example # { - # location: - # contact: - # chassis_id: + # location: , + # contact: , + # chassis_id: , # source_interface: # } # diff --git a/lib/rbeapi/api/stp.rb b/lib/rbeapi/api/stp.rb index caf9926..bd2a25f 100644 --- a/lib/rbeapi/api/stp.rb +++ b/lib/rbeapi/api/stp.rb @@ -48,13 +48,20 @@ class Stp < Entity # @example # { # mode: - # instances: {...} - # interfaces: {...} + # instances: { + # : { + # priority: + # } + # } + # interfaces: { + # : { + # portfast: , + # portfast_type: , + # bpduguard: + # } + # } # } # - # @see StpInstances instances get example - # @eee StpInterfaces interfaces get example - # # @return [Hash] returns a Hash of attributes derived from eAPI def get response = {} @@ -150,6 +157,8 @@ class StpInstances < Entity # priority: # } # + # @param [String] :inst The named stp instance to return + # # @return [nil, Hash: {...} + # : { + # priority: + # }, + # : { + # priority: + # }, + # ... # } # - # @see get Instance get example - # # @return [Hash] returns all configured stp instances # found in the nodes running configuration def getall @@ -265,10 +278,14 @@ class StpInterfaces < Entity # # @example # { - # portfast: [true, false] - # bpduguard: [true, false] + # portfast: , + # portfast_type: , + # bpduguard: # } # + # @param [String] :name The interface name to return a resource for from + # the nodes configuration + # # @return [nil, Hash] returns the stp interface as a # resource hash def get(name) @@ -289,11 +306,19 @@ def get(name) # # @example # { - # : {...} + # : { + # portfast: , + # portfast_type: , + # bpduguard: + # }, + # : { + # portfast: , + # portfast_type: , + # bpduguard: + # }, + # ... # } # - # @see get Interface example - # # @return [Hash] returns the stp interfaces config as a # resource hash from the nodes running configuration def getall diff --git a/lib/rbeapi/api/switchports.rb b/lib/rbeapi/api/switchports.rb index 16c89af..b10ed7b 100644 --- a/lib/rbeapi/api/switchports.rb +++ b/lib/rbeapi/api/switchports.rb @@ -55,7 +55,7 @@ class Switchports < Entity # "access_vlan": # } # - # @param [String] name The full name of the interface to get. The + # @param [String] :name The full name of the interface to get. The # interface name must be the full interface (ie Ethernet, not Et) # # @return [Hash] a hash that includes the switchport properties @@ -73,24 +73,63 @@ def get(name) response end + ## + # parse_mode parses switchport mode from the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_mode(config) mdata = /(?<=\s{3}switchport\smode\s)(.+)$/.match(config) { mode: mdata[1] } end private :parse_mode + ## + # parse_access_vlan parses access vlan from the provided + # config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_access_vlan(config) mdata = /(?<=access\svlan\s)(.+)$/.match(config) { access_vlan: mdata[1] } end private :parse_access_vlan + ## + # parse_trunk_native_vlan parses trunk native vlan from + # the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_trunk_native_vlan(config) mdata = /(?<=trunk\snative\svlan\s)(.+)$/.match(config) { trunk_native_vlan: mdata[1] } end private :parse_trunk_native_vlan + ## + # parse_trunk_allowed_vlans parses trunk allowed vlan from + # the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_trunk_allowed_vlans(config) mdata = /(?<=trunk\sallowed\svlan\s)(.+)$/.match(config) return { trunk_allowed_vlans: [] } unless mdata[1] != 'none' @@ -107,6 +146,16 @@ def parse_trunk_allowed_vlans(config) end private :parse_trunk_allowed_vlans + ## + # parse_trunk_groups parses trunk group values from the + # provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_trunk_groups(config) mdata = config.scan(/(?<=trunk\sgroup\s)(.+)$/) mdata = mdata.flatten if mdata.length > 0 @@ -117,6 +166,25 @@ def parse_trunk_groups(config) ## # Retrieves all switchport interfaces from the running-config # + # @example + # { + # : { + # mode: , + # access_vlan: , + # trunk_native_vlan: , + # trunk_allowed_vlans: , + # trunk_groups: + # }, + # : { + # mode: , + # access_vlan: , + # trunk_native_vlan: , + # trunk_allowed_vlans: , + # trunk_groups: + # }, + # ... + # } + # # @return [Array] an array of switchport hashes def getall interfaces = config.scan(/(?<=^interface\s)([Et|Po].+)$/) diff --git a/lib/rbeapi/api/system.rb b/lib/rbeapi/api/system.rb index 45fceaa..d93724b 100644 --- a/lib/rbeapi/api/system.rb +++ b/lib/rbeapi/api/system.rb @@ -42,11 +42,12 @@ module Api # hostname and domain name class System < Entity ## - # Returns the system settings + # Returns the system settings for hostname and iprouting # # @example # { - # hostname: + # hostname: , + # iprouting: # } # # @return [Hash] A Ruby hash object that provides the system settings as @@ -58,11 +59,29 @@ def get response end + ## + # parse_hostname parses hostname values from the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_hostname(config) mdata = /(?<=^hostname\s)(.+)$/.match(config) { hostname: mdata.nil? ? '' : mdata[1] } end + ## + # parse_iprouting parses ip routing from the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_iprouting(config) mdata = /no\sip\srouting/.match(config) { iprouting: mdata.nil? ? true : false } diff --git a/lib/rbeapi/api/tacacs.rb b/lib/rbeapi/api/tacacs.rb index b3e9cf9..4b34ae5 100644 --- a/lib/rbeapi/api/tacacs.rb +++ b/lib/rbeapi/api/tacacs.rb @@ -63,15 +63,14 @@ class Tacacs < Entity # This method is intended to be used by a provider's instances class # method. # - # The resource hash returned contains the following information: - # * name: ('settings') - # * enable: (true | false) if tacacs functionality is enabled. This is - # always true for EOS. - # * key: (String) the key either in plain text or hashed format - # * key_format: (Integer) e.g. 0 or 7 - # * timeout: (Integer) seconds before the timeout period ends - # - # @api public + # @example + # { + # name: , + # enable: , + # key: , + # key_format: , + # timeout: + # } # # @return [Array] Single element Array of resource hashes def get diff --git a/lib/rbeapi/api/users.rb b/lib/rbeapi/api/users.rb index d4cadd4..593157e 100644 --- a/lib/rbeapi/api/users.rb +++ b/lib/rbeapi/api/users.rb @@ -76,7 +76,7 @@ def initialize(node) # sshkey: # } # - # @param [String] name The user name to return a resource for from the + # @param [String] :name The user name to return a resource for from the # nodes configuration # # @return [nil, Hash] Returns the user resource as a @@ -114,6 +114,15 @@ def get(name) # secret: , # sshkey: # }, + # : { + # name: , + # privilege: , + # role: , + # nopassword: , + # encryption: <'cleartext', 'md5', 'sha512'> + # secret: , + # sshkey: + # }, # ... # ] # diff --git a/lib/rbeapi/api/varp.rb b/lib/rbeapi/api/varp.rb index 09f9ec6..45a4dcd 100644 --- a/lib/rbeapi/api/varp.rb +++ b/lib/rbeapi/api/varp.rb @@ -44,10 +44,18 @@ class Varp < Entity ## # Returns the global VARP configuration from the node # - # Example + # @example # { - # "mac_address": , - # "interfaces": {...} + # mac_address: , + # interfaces: { + # : { + # addresses: + # }, + # : { + # addresses: + # }, + # ... + # } # } # # @return [Hash] A Ruby hash object that provides the Varp settings as @@ -59,6 +67,15 @@ def get response end + ## + # parse_mac_address parses mac-address values from the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_mac_address(config) # ip virtual-router mac-address value will always # be stored in aa:bb:cc:dd:ee:ff format @@ -97,7 +114,7 @@ class VarpInterfaces < Entity ## # Returns a single VARP interface configuration # - # Example + # @example # { # "addresses": array # } @@ -119,10 +136,15 @@ def get(name) # Returns the collection of MLAG interfaces as a hash index by the # interface name # - # Example + # @example # { - # "name": {...}, - # "name": {...} + # : { + # addresses: + # }, + # : { + # addresses: + # }, + # ... # } # # @return [nil, Hash] A Ruby hash that represents the @@ -138,6 +160,16 @@ def getall end end + ## + # parse_addresses parses ip virtual-router address from the provided + # config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_addresses(config) addrs = config.scan(/(?<=\s{3}ip\svirtual-router\saddress\s).+$/) { addresses: addrs } diff --git a/lib/rbeapi/api/vlans.rb b/lib/rbeapi/api/vlans.rb index 4934c05..5567030 100644 --- a/lib/rbeapi/api/vlans.rb +++ b/lib/rbeapi/api/vlans.rb @@ -50,12 +50,12 @@ class Vlans < Entity # # @example # { - # name: - # state: + # name: , + # state: , # trunk_groups: array[] Returns the vlan resource as a @@ -78,7 +78,17 @@ def get(id) # # @example # { - # : {...} + # : { + # name: , + # state: , + # trunk_groups: array[: { + # name: , + # state: , + # trunk_groups: array[ Date: Sat, 9 Jan 2016 11:46:07 -0700 Subject: [PATCH 33/41] Making changes for auto doc generation. --- Rakefile | 19 ++ guide/Makefile | 177 ++++++++++++++ guide/_static/arista_logo_11-trans-w.png | Bin 0 -> 26494 bytes guide/_static/arista_logo_jpg-11.jpg | Bin 0 -> 45261 bytes guide/_static/favicon.ico | Bin 0 -> 4286 bytes guide/conf.py | 279 +++++++++++++++++++++++ guide/cookbook.rst | 4 + guide/developing.rst | 4 + guide/faq.rst | 4 + guide/index.rst | 23 ++ guide/installation.rst | 4 + guide/license.rst | 5 + guide/overview.rst | 20 ++ guide/quickstart.rst | 4 + guide/release-notes.rst | 6 + guide/testing.rst | 4 + guide/troubleshooting.rst | 1 + 17 files changed, 554 insertions(+) create mode 100644 guide/Makefile create mode 100644 guide/_static/arista_logo_11-trans-w.png create mode 100644 guide/_static/arista_logo_jpg-11.jpg create mode 100644 guide/_static/favicon.ico create mode 100644 guide/conf.py create mode 100644 guide/cookbook.rst create mode 100644 guide/developing.rst create mode 100644 guide/faq.rst create mode 100644 guide/index.rst create mode 100644 guide/installation.rst create mode 100644 guide/license.rst create mode 100644 guide/overview.rst create mode 100644 guide/quickstart.rst create mode 100644 guide/release-notes.rst create mode 100644 guide/testing.rst create mode 100644 guide/troubleshooting.rst diff --git a/Rakefile b/Rakefile index 27626c2..f41752a 100644 --- a/Rakefile +++ b/Rakefile @@ -155,3 +155,22 @@ task ci_spec: [:ci_prep, 'ci:setup:rspec', :spec] require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) + +desc 'Generate typedoc.rst for the guide' +task :typedoc do + system 'rbeapi doc -r type \ + | awk \'/This page/{flag=1}/augeas/{flag=0}/eos_/{flag=1}/ exec/\ + {flag=0}/\*This page/{flag=1}flag\' \ + | pandoc --from=markdown --to=rst --output=- \ + > guide/typedoc.rst' +end + +desc 'Generate Getting Started Guide HTML' +task guide: [:typedoc] do + system 'make -C guide html' +end + +desc 'Clean Getting Started docs' +task :guide_clean do + system 'make -C guide clean' +end diff --git a/guide/Makefile b/guide/Makefile new file mode 100644 index 0000000..1985178 --- /dev/null +++ b/guide/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Rbeapi.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Rbeapi.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Rbeapi" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Rbeapi" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/guide/_static/arista_logo_11-trans-w.png b/guide/_static/arista_logo_11-trans-w.png new file mode 100644 index 0000000000000000000000000000000000000000..dec3830240e8774e8036da4e39f15fc024d348f3 GIT binary patch literal 26494 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 literal 0 HcmV?d00001 diff --git a/guide/_static/favicon.ico b/guide/_static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cf55b0c85d981ebce5ef33b723be1ede8f26cbc1 GIT binary patch literal 4286 zcmeH|yK7WI6o+SnCWS<;)Fi^D5FfQ7iHK{FbShZ*2Z&K(VX64Q-8NtmA7CTcCLqKn zWe~L4LJ=QmXOYCxM?#2RaNY4c!GXhJW*>J$s=eXM+;ir4e&^oFor!Y=eujsg{C2au zo!jG_I}D`UX^?#P>aIX+1tyQVIryP?&3#`?+er@3e)z2VNYAHm2!4ZClsoAatHt}i z@|RveVMw%$cg$QyEL&((F1(II&GeWI=Hz?fgPDIx+!Hi;M?AK}JLCPzdJ~VrU!%L6 zw{NJ!@0RgQt}VJpMl0XQK=5jJr1l{EGM;x+KInGBlF`o-UueRZ^+aEQ7oflEkHo!J zt7GQRnHpW6x|hG(3FC9g_{jG+sx`5DG_oI_nc7reotN)-!SB!`Be*)JUFu~keu+MrX)hCt5qX*;+&|Ytdw>7Ad zGgjVJ*US*-x~Mw>vR}2|^^_U92S)picyCJIN}l?%@mwc9U~9>ZfIVX`T3x#_o_+8M z^cu*%E(9;`<@|ncGz(f2w0TB+PJ%s~CxRDqw?RK&bd7>O&xrqH(?j;>AjlbO|9{`e z)1y%hJ$AuY(C_)2*q*U($tCwlqaMxX@I7z(>t~VsKX9~}Pu_XFaywe}=|o4LtEPXQc)SzOc53M*XY&-SfviRHOl-%utL?79R v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +html_logo = '_static/arista_logo_11-trans-w.png' + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = '_static/favicon.ico' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'rbeapidoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'rbeapi.tex', u'Rbeapi Documentation', + u'Arista Networks - EOS+ Consulting Services', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +latex_logo = '_static/arista_logo_jpg-11.jpg' + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'rbeapi', u'Rbeapi Documentation', + [u'Arista Networks - EOS+ Consulting Services'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'Rbeapi', u'Rbeapi Documentation', + u'Arista Networks - EOS+ Consulting Services', 'Rbeapi', 'Ruby API for EOS.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/guide/cookbook.rst b/guide/cookbook.rst new file mode 100644 index 0000000..c5535bd --- /dev/null +++ b/guide/cookbook.rst @@ -0,0 +1,4 @@ +Cookbook +============ + +.. contents:: :local: \ No newline at end of file diff --git a/guide/developing.rst b/guide/developing.rst new file mode 100644 index 0000000..5e7e5e4 --- /dev/null +++ b/guide/developing.rst @@ -0,0 +1,4 @@ +Developing +========== + +.. contents:: :local: \ No newline at end of file diff --git a/guide/faq.rst b/guide/faq.rst new file mode 100644 index 0000000..eb5392d --- /dev/null +++ b/guide/faq.rst @@ -0,0 +1,4 @@ +FAQ +=== + +.. contents:: :local: \ No newline at end of file diff --git a/guide/index.rst b/guide/index.rst new file mode 100644 index 0000000..fb21811 --- /dev/null +++ b/guide/index.rst @@ -0,0 +1,23 @@ +.. Rbeapi documentation master file, created by + sphinx-quickstart on Fri Jan 8 11:04:02 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Rbeapi's documentation! +================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + overview + quickstart + installation + cookbook + troubleshooting + developing + testing + faq + release-notes + license \ No newline at end of file diff --git a/guide/installation.rst b/guide/installation.rst new file mode 100644 index 0000000..3c4aa04 --- /dev/null +++ b/guide/installation.rst @@ -0,0 +1,4 @@ +Installation +============ + +.. contents:: :local: \ No newline at end of file diff --git a/guide/license.rst b/guide/license.rst new file mode 100644 index 0000000..df32f7b --- /dev/null +++ b/guide/license.rst @@ -0,0 +1,5 @@ +License +======= + +.. include:: ../LICENSE + :literal: \ No newline at end of file diff --git a/guide/overview.rst b/guide/overview.rst new file mode 100644 index 0000000..d97ec29 --- /dev/null +++ b/guide/overview.rst @@ -0,0 +1,20 @@ +Overview +======== + +.. contents:: :local: + +Introduction +------------ + +The Ruby Client for eAPI provides a native Ruby implementation for programming Arista EOS network devices using Ruby. The Ruby client provides the ability to build native applications in Ruby that can communicate with EOS either locally via Unix domain sockets (on-box) or remotely over a HTTP/S transport (off-box). It uses a standard INI-style configuration file to specifiy one or more connection profiles. + +The rbeapi implemenation also provides an API layer for building native Ruby objects that allow for configuration and state extraction of EOS nodes. The API layer provides a consistent implementation for working with EOS configuration resources. The implementation of the API layer is highly extensible and can be used as a foundation for building custom data models. + +The libray is freely provided to the open source community for building robust applications using Arista EOS eAPI. Support is provided as best effort through Github iusses. + +Prerequisites +------------- + +* Arista EOS 4.12 or later +* Arista eAPI enabled for at least one transport (see official EOS Config Guide at arista.com for details) +* Ruby 1.9.3 or later \ No newline at end of file diff --git a/guide/quickstart.rst b/guide/quickstart.rst new file mode 100644 index 0000000..41feb59 --- /dev/null +++ b/guide/quickstart.rst @@ -0,0 +1,4 @@ +Quick Start +=========== + +.. contents:: :local: \ No newline at end of file diff --git a/guide/release-notes.rst b/guide/release-notes.rst new file mode 100644 index 0000000..31b3783 --- /dev/null +++ b/guide/release-notes.rst @@ -0,0 +1,6 @@ +Release Notes +============= + +.. toctree:: + :maxdepth: 2 + :titlesonly: \ No newline at end of file diff --git a/guide/testing.rst b/guide/testing.rst new file mode 100644 index 0000000..79ab0db --- /dev/null +++ b/guide/testing.rst @@ -0,0 +1,4 @@ +Testing Modules +=============== + +.. contents:: :local: \ No newline at end of file diff --git a/guide/troubleshooting.rst b/guide/troubleshooting.rst new file mode 100644 index 0000000..3fa493d --- /dev/null +++ b/guide/troubleshooting.rst @@ -0,0 +1 @@ +.. _troubleshooting: \ No newline at end of file From 99e9106857d2dd9c9c17d47e9c1a42e48e042e59 Mon Sep 17 00:00:00 2001 From: John Corbin Date: Mon, 11 Jan 2016 07:37:10 -0800 Subject: [PATCH 34/41] Add terminal to configure command to work around AAA issue found in pyeapi. --- lib/rbeapi/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rbeapi/client.rb b/lib/rbeapi/client.rb index 640c0dd..6e0f069 100644 --- a/lib/rbeapi/client.rb +++ b/lib/rbeapi/client.rb @@ -332,7 +332,7 @@ def enable_authentication(password) def config(commands, opts = {}) commands = [*commands] unless commands.respond_to?('each') - commands.insert(0, 'configure') + commands.insert(0, 'configure terminal') if @dry_run puts '[rbeapi dry-run commands]' From 5f14b37db94ca64f3676b5aa87a0787e2cdb55f6 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 11 Jan 2016 11:33:46 -0700 Subject: [PATCH 35/41] Adding missing documentation. --- lib/rbeapi/api/dns.rb | 26 +++++++++++- lib/rbeapi/api/ospf.rb | 79 +++++++++++++++++++++++++++++++++++ lib/rbeapi/api/prefixlists.rb | 27 ++++++++++++ lib/rbeapi/api/snmp.rb | 9 ++++ 4 files changed, 140 insertions(+), 1 deletion(-) diff --git a/lib/rbeapi/api/dns.rb b/lib/rbeapi/api/dns.rb index 2f95494..09089ae 100644 --- a/lib/rbeapi/api/dns.rb +++ b/lib/rbeapi/api/dns.rb @@ -41,7 +41,7 @@ module Api # The Dns class manages DNS settings on an EOS node. class Dns < Entity ## - # Returns the DNS resource + # get returns the DNS resource # # @example # { @@ -154,10 +154,22 @@ def set_name_servers(opts = {}) configure cmds end + ## + # add_name_server adds an ip name-server. + # + # @param [String] :server The name of the ip name-server to create + # + # @return [Boolean] returns true if the command completed successfully def add_name_server(server) configure "ip name-server #{server}" end + ## + # remove_name_server removes the specified ip name-server. + # + # @param [String] :server The name of the ip name-server to remove + # + # @return [Boolean] returns true if the command completed successfully def remove_name_server(server) configure "no ip name-server #{server}" end @@ -209,10 +221,22 @@ def set_domain_list(opts = {}) configure cmds end + ## + # add_domain_list adds an ip domain-list. + # + # @param [String] :name The name of the ip domain-list to add + # + # @return [Boolean] returns true if the command completed successfully def add_domain_list(name) configure "ip domain-list #{name}" end + ## + # remove_domain_list removes a specified ip domain-list. + # + # @param [String] :name The name of the ip domain-list to remove + # + # @return [Boolean] returns true if the command completed successfully def remove_domain_list(name) configure "no ip domain-list #{name}" end diff --git a/lib/rbeapi/api/ospf.rb b/lib/rbeapi/api/ospf.rb index 8772825..8f4fea4 100644 --- a/lib/rbeapi/api/ospf.rb +++ b/lib/rbeapi/api/ospf.rb @@ -117,28 +117,91 @@ def interfaces @interfaces end + ## + # create will create a router ospf with the specified pid + # + # @param [String] :pid The router ospf to create + # + # @return [Boolean] returns true if the command completed successfully def create(pid) configure "router ospf #{pid}" end + ## + # delete will remove the specified router ospf + # + # @param [String] :pid The router ospf to remove + # + # @return [Boolean] returns true if the command completed successfully def delete(pid) configure "no router ospf #{pid}" end + ## + # set_router_id sets router ospf router-id with pid and options + # + # @param [String] :pid The router ospf name + # + # @param [hash] :opts Optional keyword arguments + # + # @option :opts [Boolean] :enable If false then the command is + # negated. Default is true. + # + # @option :opts [Boolean] :default Configure the router-id to default. + # + # @return [Boolean] returns true if the command completed successfully def set_router_id(pid, opts = {}) cmd = command_builder('router-id', opts) cmds = ["router ospf #{pid}", cmd] configure cmds end + ## + # add_network adds network settings for router ospf and network area. + # + # @param [String] :pid The pid for router ospf + # + # @param [String] :net The network name + # + # @param [String] :area The network area name + # + # @return [Boolean] returns true if the command completed successfully def add_network(pid, net, area) configure ["router ospf #{pid}", "network #{net} area #{area}"] end + ## + # remove_network removes network settings for router ospf and network + # area. + # + # @param [String] :pid The pid for router ospf + # + # @param [String] :net The network name + # + # @param [String] :area The network area name + # + # @return [Boolean] returns true if the command completed successfully def remove_network(pid, net, area) configure ["router ospf #{pid}", "no network #{net} area #{area}"] end + ## + # set_redistribute sets router ospf router-id with pid and options + # + # @param [String] :pid The router ospf name + # + # @param [String] :proto The redistribute value + # + # @param [hash] :opts Optional keyword arguments + # + # @option :opts [String] :routemap The route-map value + # + # @option :opts [Boolean] :enable If false then the command is + # negated. Default is true. + # + # @option :opts [Boolean] :default Configure the router-id to default. + # + # @return [Boolean] returns true if the command completed successfully def set_redistribute(pid, proto, opts = {}) routemap = opts[:routemap] cmds = ["router ospf #{pid}", "redistribute #{proto}"] @@ -202,6 +265,22 @@ def getall end end + ## + # set_network_type sets network type with options + # + # @param [String] :name The name of the interface + # + # @param [hash] :opts Optional keyword arguments + # + # @option :opts [String] :value The point-to-point value + # + # @option :opts [Boolean] :enable If false then the command is + # negated. Default is true. + # + # @option :opts [Boolean] :default Configure the ip ospf network + # to default. + # + # @return [Boolean] returns true if the command completed successfully def set_network_type(name, opts = {}) value = opts[:value] return false unless [nil, 'point-to-point'].include?(value) diff --git a/lib/rbeapi/api/prefixlists.rb b/lib/rbeapi/api/prefixlists.rb index 1c42c31..9e076fa 100644 --- a/lib/rbeapi/api/prefixlists.rb +++ b/lib/rbeapi/api/prefixlists.rb @@ -93,10 +93,29 @@ def getall end end + ## + # create will create a new ip prefix-list with designated name. + # + # @param [String] :name The name of the ip prefix-list + # + # @return [Boolean] returns true if the command completed successfully def create(name) configure "ip prefix-list #{name}" end + ## + # add_rule will create an ip prefix-list with the designated name, + # seqno, action and prefix. + # + # @param [String] :name The name of the ip prefix-list + # + # @param [String] :seq The seq value + # + # @param [String] :action The action value + # + # @param [String] :prefix The prefix value + # + # @return [Boolean] returns true if the command completed successfully def add_rule(name, action, prefix, seq = nil) cmd = "ip prefix-list #{name}" cmd << " seq #{seq}" if seq @@ -104,6 +123,14 @@ def add_rule(name, action, prefix, seq = nil) configure cmd end + ## + # delete will remove the designated prefix-list + # + # @param [String] :name The name of the ip prefix-list + # + # @param [String] :seq The seq value + # + # @return [Boolean] returns true if the command completed successfully def delete(name, seq = nil) cmd = "no ip prefix-list #{name}" cmd << " seq #{seq}" if seq diff --git a/lib/rbeapi/api/snmp.rb b/lib/rbeapi/api/snmp.rb index 6f24e44..3006260 100644 --- a/lib/rbeapi/api/snmp.rb +++ b/lib/rbeapi/api/snmp.rb @@ -403,6 +403,15 @@ def set_community_acl(name, opts = {}) configure cmds end + ## + # set_community_access configures snmp-server community with designated + # name and access values. + # + # @param [String] :name The snmp-server community name value + # + # @param [String] :access The snmp-server community access value + # + # @return [Boolean] returns true if the command completed successfully def set_community_access(name, access) configure "snmp-server community #{name} #{access}" end From ae4cd44f708418e8db7139edf65954391a841b81 Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 11 Jan 2016 11:41:17 -0700 Subject: [PATCH 36/41] rubocop issues. --- lib/rbeapi/api/prefixlists.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rbeapi/api/prefixlists.rb b/lib/rbeapi/api/prefixlists.rb index 9e076fa..0a2435d 100644 --- a/lib/rbeapi/api/prefixlists.rb +++ b/lib/rbeapi/api/prefixlists.rb @@ -97,7 +97,7 @@ def getall # create will create a new ip prefix-list with designated name. # # @param [String] :name The name of the ip prefix-list - # + # # @return [Boolean] returns true if the command completed successfully def create(name) configure "ip prefix-list #{name}" @@ -114,7 +114,7 @@ def create(name) # @param [String] :action The action value # # @param [String] :prefix The prefix value - # + # # @return [Boolean] returns true if the command completed successfully def add_rule(name, action, prefix, seq = nil) cmd = "ip prefix-list #{name}" From 8e6b1b53e25fba7213bf10876b6f79cfc9c3582d Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 11 Jan 2016 11:44:55 -0700 Subject: [PATCH 37/41] Change this back. --- lib/rbeapi/api/vrrp.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rbeapi/api/vrrp.rb b/lib/rbeapi/api/vrrp.rb index a8b1b1d..69cf370 100644 --- a/lib/rbeapi/api/vrrp.rb +++ b/lib/rbeapi/api/vrrp.rb @@ -32,7 +32,7 @@ require 'rbeapi/api' ## -# Rbeapi toplevel namespace +# Eos is the toplevel namespace for working with Arista EOS nodes module Rbeapi ## # Api is module namespace for working with the EOS command API From 47983e8a327d88cec21221a2d2ef2665af40228d Mon Sep 17 00:00:00 2001 From: Hunt Burdick Date: Mon, 11 Jan 2016 13:40:09 -0700 Subject: [PATCH 38/41] Update copyright year and getall count to spec. --- spec/unit/rbeapi/api/switchports/default_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/unit/rbeapi/api/switchports/default_spec.rb b/spec/unit/rbeapi/api/switchports/default_spec.rb index d34dccf..6217276 100644 --- a/spec/unit/rbeapi/api/switchports/default_spec.rb +++ b/spec/unit/rbeapi/api/switchports/default_spec.rb @@ -1,5 +1,5 @@ # -# Copyright (c) 2015, Arista Networks, Inc. +# Copyright (c) 2016, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -90,6 +90,10 @@ def switchports it 'returns a hash collection' do expect(subject.getall).to be_a_kind_of(Hash) end + + it 'returns a hash collection' do + expect(subject.getall.count).to eq(1) + end end describe '#create' do From 61f99bce1e1936c819699b7d4c107fe6c444bc2f Mon Sep 17 00:00:00 2001 From: John Corbin Date: Mon, 11 Jan 2016 13:15:26 -0800 Subject: [PATCH 39/41] Copy configuration entry before modifying with connection specific info. Fixes issue #86 --- lib/rbeapi/client.rb | 5 ++-- spec/fixtures/wildcard.conf | 43 +++++++++++++++++++++++++++++++++ spec/unit/rbeapi/client_spec.rb | 26 ++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 spec/fixtures/wildcard.conf diff --git a/lib/rbeapi/client.rb b/lib/rbeapi/client.rb index dac0e44..ed2bd16 100644 --- a/lib/rbeapi/client.rb +++ b/lib/rbeapi/client.rb @@ -99,8 +99,9 @@ def config_for(name) # @return [Rbeapi::Node, nil] Returns an instance of Rbeapi::Node. If # the named configuration is not found then nil is returned def connect_to(name) - config = config_for(name) - return nil unless config + config_entry = config_for(name) + return nil unless config_entry + config = config_entry.dup config['host'] = name if config['host'] == '*' config = Rbeapi::Utils.transform_keys_to_symbols(config) connection = connect config diff --git a/spec/fixtures/wildcard.conf b/spec/fixtures/wildcard.conf new file mode 100644 index 0000000..69a3e65 --- /dev/null +++ b/spec/fixtures/wildcard.conf @@ -0,0 +1,43 @@ +[connection:veos01] +username: eapi +password: password +transport: http +host: veos01 + +[connection:veos02] +transport: http +host: veos02 + +[connection:veos03] +transport: socket +host: veos03 + +[connection:veos04] +host: 172.16.10.1 +username: eapi +password: password +enablepwd: itsasecret +port: 1234 +transport: https + +[connection:veos05] +host: 172.16.131.40 +username: admin +password: admin +enablepwd: password +transport: https +port: 1234 +open_timeout: 12 +read_timeout: 12 + +[connection: localhost] +transport: http_local +host: localhost + +[connection:localhost] +transport: socket +host: localhost + +[connection:*] +username: foo +password: bar diff --git a/spec/unit/rbeapi/client_spec.rb b/spec/unit/rbeapi/client_spec.rb index 51de6a6..4af7038 100644 --- a/spec/unit/rbeapi/client_spec.rb +++ b/spec/unit/rbeapi/client_spec.rb @@ -54,6 +54,10 @@ def yaml_conf fixture_file('eapi.conf.yaml') end + def wildcard_conf + fixture_file('wildcard.conf') + end + let(:dut) do File.read(dut_conf) end @@ -84,6 +88,14 @@ def yaml_conf } end + let(:wildcard) do + { + 'username' => 'foo', + 'password' => 'bar', + 'host' => '*' + } + end + let(:test_data) do [ '[connection:veos01]' @@ -116,6 +128,14 @@ def yaml_conf it 'retrieves the node config' do expect(subject.connect_to('veos01')).to be_truthy end + + it 'check connection wildcard host name' do + expect(subject.load_config(wildcard_conf)).to eq(nil) + expect(subject.connect_to('host1')).to be_truthy + expect(subject.config.get_connection('host1')) .to eq(wildcard) + expect(subject.connect_to('host2')).to be_truthy + expect(subject.config.get_connection('host2')) .to eq(wildcard) + end end describe '#load_config' do @@ -156,6 +176,12 @@ def yaml_conf it 'get connection dut' do expect(subject.config.get_connection('veos01')).to eq(veos01) end + + it 'connection wildcard works' do + expect(subject.load_config(wildcard_conf)).to eq(nil) + expect(subject.config.get_connection('host1')) .to eq(wildcard) + expect(subject.config.get_connection('host2')) .to eq(wildcard) + end end describe '#reload' do From 1dab3ef15528093f56a19abd02ac152897a29507 Mon Sep 17 00:00:00 2001 From: Jere Julian Date: Tue, 12 Jan 2016 06:19:14 -0500 Subject: [PATCH 40/41] Bump version strings --- Gemfile | 2 +- README.md | 22 +++++++++++----------- lib/rbeapi/version.rb | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Gemfile b/Gemfile index 4d206b8..548e7a7 100644 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,7 @@ group :development, :test do gem 'pry', require: false gem 'pry-doc', require: false gem 'pry-stack_explorer', require: false - gem 'rbeapi', '0.4.0', path: '.' + gem 'rbeapi', '0.5.0', path: '.' gem 'ci_reporter_rspec', require: false gem 'simplecov-json', require: false gem 'simplecov-rcov', require: false diff --git a/README.md b/README.md index e0790cb..311a565 100644 --- a/README.md +++ b/README.md @@ -230,21 +230,21 @@ Copy the RPMs to an EOS device then run the 'swix create' command. Examples: Puppet Open Source: cd /mnt/flash; \ - swix create rbeapi-0.4.0-1.swix \ - rubygem-rbeapi-0.4.0-1.eos4.noarch.rpm \ + swix create rbeapi-0.5.0-1.swix \ + rubygem-rbeapi-0.5.0-1.eos4.noarch.rpm \ rubygem-inifile-3.0.0-3.eos4.noarch.rpm \ rubygem-netaddr-1.5.0-2.eos4.noarch.rpm \ rubygem-net_http_unix-0.2.1-3.eos4.noarch.rpm Puppet-enterprise agent (3.x): cd/mnt/flash; \ - swix create rbeapi-puppet3-0.4.0-1.swix \ - rubygem-rbeapi-puppet3-0.4.0-1.eos4.noarch.rpm \ + swix create rbeapi-puppet3-0.5.0-1.swix \ + rubygem-rbeapi-puppet3-0.5.0-1.eos4.noarch.rpm \ rubygem-inifile-puppet3-3.0.0-3.eos4.noarch.rpm \ rubygem-netaddr-puppet3-1.5.0-2.eos4.noarch.rpm Puppet-All-in-one agent (2015.x/4.x): cd/mnt/flash; \ - swix create rbeapi-puppet-aio-0.4.0-1.swix \ - rubygem-rbeapi-puppet-aio-0.4.0-1.eos4.noarch.rpm \ + swix create rbeapi-puppet-aio-0.5.0-1.swix \ + rubygem-rbeapi-puppet-aio-0.5.0-1.eos4.noarch.rpm \ rubygem-inifile-puppet-aio-3.0.0-3.eos4.noarch.rpm \ rubygem-netaddr-puppet-aio-1.5.0-2.eos4.noarch.rpm \ rubygem-net_http_unix-puppet-aio-0.2.1-3.eos4.noarch.rpm @@ -255,13 +255,13 @@ Copy the RPMs to an EOS device then run the 'swix create' command. Arista# copy flash: Arista# bash -bash-4.1# cd /mnt/flash/ - -bash-4.1# swix create rbeapi-puppet3-0.4.0-1.swix \ - rubygem-rbeapi-puppet3-0.4.0-1.eos4.noarch.rpm \ + -bash-4.1# swix create rbeapi-puppet3-0.5.0-1.swix \ + rubygem-rbeapi-puppet3-0.5.0-1.eos4.noarch.rpm \ rubygem-inifile-puppet3-3.0.0-1.eos4.noarch.rpm \ rubygem-netaddr-puppet3-1.5.0-1.eos4.noarch.rpm -bash-4.1# exit - Arista# copy flash:rbeapi-puppet3-0.4.0-1.swix extension: - Arista# extension rbeapi-puppet3-0.4.0-1.swix + Arista# copy flash:rbeapi-puppet3-0.5.0-1.swix extension: + Arista# extension rbeapi-puppet3-0.5.0-1.swix Arista# copy installed-extensions boot-extensions ``` @@ -270,7 +270,7 @@ Copy the RPMs to an EOS device then run the 'swix create' command. On EOS: ``` Arista# no extension pe-rbeapi-0.3.0-1.swix - Arista# extension rbeapi-puppet3-0.4.0-1.swix + Arista# extension rbeapi-puppet3-0.5.0-1.swix Arista# copy installed-extensions boot-extensions ``` diff --git a/lib/rbeapi/version.rb b/lib/rbeapi/version.rb index d179c37..53c83dc 100644 --- a/lib/rbeapi/version.rb +++ b/lib/rbeapi/version.rb @@ -33,5 +33,5 @@ # # # Rbeapi toplevel namespace module Rbeapi - VERSION = '0.4.0' + VERSION = '0.5.0' end From 190971060875c200fc5b151d3cd3e4c290b78533 Mon Sep 17 00:00:00 2001 From: Jere Julian Date: Tue, 12 Jan 2016 11:23:09 -0500 Subject: [PATCH 41/41] Update CHANGELOG / release notes --- CHANGELOG.md | 20 ++++++++++++ guide/release-notes-0.5.0.rst | 60 +++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 guide/release-notes-0.5.0.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index c88f90c..24b152b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,26 @@ Ruby Client for eAPI ==================== +## v0.5.0, January, 2016 + +- Add optional ‘mode’ parameter to set_members() method in port-channel + interfaces API +- Add support for trunk groups +- Ensure multiple connections based on the wildcard settings do not clobber + each other. +- Add ‘terminal’ to the ‘configure’ command to workaround AAA issue +- Fix issue where ‘enablepw’ in the eapi.conf was not properly used +- Catch errors and syslog them when parsing eapi conf file. + In the event of an unparsable eapi.conf, a syslog warning will be generated + but the app will continue to attempt to utilize the default localhost conn. +- Ensure that nil is returned when getting nonexistent username +- Ensure all parse methods are private +- Add tests for timeout values +- Update framework tests +- Add unit tests for switchports +- Address code coverage gaps + + ## v0.4.0, November, 2015 - New users API diff --git a/guide/release-notes-0.5.0.rst b/guide/release-notes-0.5.0.rst new file mode 100644 index 0000000..13c3b4e --- /dev/null +++ b/guide/release-notes-0.5.0.rst @@ -0,0 +1,60 @@ +Release 0.5.0 - January 2016 +---------------------------- + +.. contents:: :local: + +Enhancements +^^^^^^^^^^^^ + +* Add lacp_mode option when setting port-channel members. (`89 `_) [`devrobo `_] + .. comment +* Add support for trunk groups. (`88 `_) [`devrobo `_] + .. comment +* Unit tests for switchports (`94 `_) [`websitescenes `_] + .. comment +* Ensure all parse methods are private. (`93 `_) [`websitescenes `_] + .. comment +* Add tests for timeout values (`92 `_) [`websitescenes `_] + .. comment +* Relax check on getall entries (`91 `_) [`devrobo `_] + .. comment +* Update framework tests (`90 `_) [`websitescenes `_] + .. comment +* Add basic framework tests. (`85 `_) [`websitescenes `_] + .. comment +* Address code coverage gaps (`84 `_) [`websitescenes `_] + .. comment + +Fixed +^^^^^ + +* Copy configuration entry before modifying with connection specific info. (`101 `_) + .. comment +* Add 'terminal' to configure command to work around AAA issue. (`99 `_) + .. comment +* Set enable password for a connection. (`96 `_) + .. comment +* Catch errors and syslog them when parsing eapi conf file. (`95 `_) + In the event of an unparsable ``eapi.conf`` file, which could occur due to other tools which used a YAML syntax instead of INI, rbeapi will log a warning via syslog, but continue to attempt a default connection to localhost. + .. comment +* Ensure that nil is returned when getting nonexistent username. (`83 `_) + .. comment +* Failure when eapi.conf is not formatted correctly (`82 `_) + In the event of an unparsable ``eapi.conf`` file, which could occur due to other tools which used a YAML syntax instead of INI, rbeapi will log a warning via syslog, but continue to attempt a default connection to localhost. +* Enable password setting in the .eapi.conf file not honored (`72 `_) + ``enablepwd`` is now properly used, if defined, in the ``eapi.conf`` +* API interfaces should accept an lacp_mode to configure for port-channel members (`58 `_) + ``set_members()`` now configures LACP mode when adding members to a port-channel + +Known Caveats +^^^^^^^^^^^^^ + +* Add support for commands with input (`100 `_) + .. comment +* Wildcard connection config gets clobbered (`86 `_) + .. comment +* Need to validate value keyword in set methods when array (`40 `_) + .. comment +* get_connect should raise an error instead of returning nil if no connection is found (`31 `_) + .. comment +