Skip to content

Latest commit

 

History

History
512 lines (392 loc) · 17.1 KB

README-develop-node-utils-APIs.md

File metadata and controls

512 lines (392 loc) · 17.1 KB

How To Create New node_utils APIs

Table of Contents

This document is a HowTo guide for writing new cisco_node_utils APIs. The APIs act as an interface between the NX-OS CLI and an agent's resource/provider. If written properly the new API will work as a common framework for multiple providers (Puppet, Chef, etc).

There are multiple components involved when creating new resources. This document focuses on the cisco_node_utils API, command reference YAML files, and minitests.

1

Please note: A virtual Nexus N9000/N3000 may be helpful for development and testing. Users with a valid cisco.com user ID can obtain a copy of a virtual Nexus N9000/N3000 by sending their cisco.com user ID in an email to [email protected]. If you do not have a cisco.com user ID please register for one at https://tools.cisco.com/IDREG/guestRegistration

This development guide uses tools that are packaged as gems that need to be installed on your server.

gem install cisco_nxapi
gem install rake
gem install rubocop
gem install simplecov
gem install minitest --version 4.3.2

NOTE: If you are working from a server where you don't have admin/root privilages, use the following commands to install the gems and then update the PATH to include ~/.gem/ruby/x.x.x/bin

gem install --user-install cisco_nxapi
gem install --user-install rake
gem install --user-install rubocop
gem install --user-install simplecov
gem install --user-install minitest --version 4.3.2

First fork the cisco-network-node-utils git repository

Next install the code base. Clone the cisco-network-node-utils repo from your fork into a workspace:

git clone https://github.com/YOUR-USERNAME/cisco-network-node-utils.git
cd cisco-network-node-utils/

Please note that any code commits must be associated with your github account and email address. If you intend to commit code to this repository then use the following commands to update your workspace with your credentials:

git config --global user.name "John Doe"
git config --global user.email [email protected]

As a best practice create a topic/feature branch for your feature work using the git branch feature/<feature_name> command.

git branch feature/eigrp
git branch
* develop
  feature/eigrp

Before you start working on the eigrp feature, checkout the feature branch you created earlier.

git checkout feature/eigrp
git branch
  develop
* feature/eigrp

router eigrp requires feature enablement and supports multiple eigrp instances. It also has multiple configuration levels for vrf and address-family.

For the purposes of this example we will only implement the following properties:

[no] feature eigrp               (boolean)
[no] router eigrp [name]         (string)
       maximum-paths [n]         (integer)
       [no] shutdown             (boolean)

Example:
  feature eigrp
  router eigrp Blue
    maximum-paths 5
    shutdown

The new API for router eigrp will need some basic YAML definitions.

command_reference_common.yaml is used for settings that are common across all platforms while other files are used for settings that are unique to a given platform. Our router eigrp example uses the same cli syntax on all platforms, thus we only need to edit the common file:

lib/cisco_node_utils/command_reference_common.yaml

Four basic command_reference parameters will be defined for each resource property:

  1. config_get: This defines the NX-OS CLI command (usually a 'show...' command) used to retrieve the property's current configuration state. Note that some commands may not be present until a feature is enabled.
  2. config_get_token: A regexp pattern for extracting state values from the config_get output.
  3. config_set: The NX-OS CLI configuration command(s) used to set the property configuration. May contain wildcards for variable parameters.
  4. default_value: This is typically the "factory" default state of the property, expressed as an actual value (true, 12, "off", etc)

There are additional YAML command parameters available which are not covered by this document. Please see the README_YAML.md document for more information on the structure and semantics of these files. The properties in this example require additional context for their config_get_token values because they need to differentiate between different eigrp instances. Most properties will also have a default value.

Note: Eigrp also has vrf and address-family contexts. These contexts require additional coding and are beyond the scope of this document.

Example: YAML Property Definitions for router eigrp

Note: The basic token definitions for multi-level commands can become long and complicated. A better solution for these commands is to use a command_reference _template: definition to simplify the configuration. The example below will use the basic syntax; see the ospf definitions in the YAML file for an example of _template: usage.

eigrp:
  feature:
    # feature eigrp must be enabled before configuring router eigrp
    config_get: 'show running eigrp all'
    config_get_token: '/^feature eigrp$/'
    config_set: '<state> feature eigrp'

  router:
    # There can be multiple eigrp instances
    config_get: 'show running eigrp all'         # all eigrp-related configs
    config_get_token: '/^router eigrp (\S+)$/'   # Match instance name
    config_set: '<state> router eigrp <name>'    # config to add or remove

  maximum_paths:
    # This is an integer property
    config_get: 'show running eigrp all'
    config_get_token: ['/^router eigrp <name>$/', '/^maximum-paths (\d+)/']
    config_set: ['router eigrp <name>', 'maximum-paths <val>']
    default_value: 8

  shutdown:
    # This is a boolean property
    config_get: 'show running eigrp all'
    config_get_token: ['/^router eigrp <name>$/', '/^shutdown$/']
    config_set: ['router eigrp <name>', '<state> shutdown']
    default_value: false
  • The template-router.rb file provides a basic router API that we will use as the basis for router_eigrp.rb:
cp  docs/template-router.rb  lib/cisco_node_utils/router_eigrp.rb
  • Our new router_eigrp.rb requires changes from the original template. Edit router_eigrp.rb and change the placeholder names as shown.
/X__CLASS_NAME__X/RouterEigrp/

/X__RESOURCE_NAME__X/eigrp/

/X__PROPERTY_BOOL__X/shutdown/

/X__PROPERTY_INT__X/maximum_paths/

Note that this template only provides example property methods for a few properties. Copy the example methods for additional properties as needed.

Example: router_eigrp.rb

This is the completed router_eigrp API based on template-router.rb:

# Copyright (c) 2014-2015 Cisco and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require_relative 'node_util'

module Cisco
  # RouterEigrp - node utility class for EIGRP config management.
  class RouterEigrp < NodeUtil
    attr_reader :name

    # name: name of the router instance
    # instantiate: true = create router instance
    def initialize(name, instantiate=true)
      fail ArgumentError unless name.length > 0
      @name = name
      create if instantiate
    end

    # Create a hash of all current router instances.
    def self.routers
      instances = config_get('eigrp', 'router')
      return {} if instances.nil?
      hash = {}
      instances.each do |name|
        hash[name] = RouterEigrp.new(name, false)
      end
      return hash
    rescue Cisco::CliError => e
      # CLI will syntax reject when feature is not enabled
      raise unless e.clierror =~ /Syntax error/
      return {}
    end

    def feature_enabled
      feat =  config_get('eigrp', 'feature')
      return !(feat.nil? || feat.empty?)
    rescue Cisco::CliError => e
      # This cmd will syntax reject if feature is not
      # enabled. Just catch the reject and return false.
      return false if e.clierror =~ /Syntax error/
      raise
    end

    def feature_enable
      config_set('eigrp', 'feature', state: '')
    end

    def feature_disable
      config_set('eigrp', 'feature', state: 'no')
    end

    # Enable feature and create router instance
    def create
      feature_enable unless feature_enabled
      eigrp_router
    end

    # Destroy a router instance; disable feature on last instance
    def destroy
      ids = config_get('eigrp', 'router')
      return if ids.nil?
      if ids.size == 1
        feature_disable
      else
        eigrp_router('no')
      end
    rescue Cisco::CliError => e
      # CLI will syntax reject when feature is not enabled
      raise unless e.clierror =~ /Syntax error/
    end

    def eigrp_router(state='')
      config_set('eigrp', 'router', name: @name, state: state)
    end

    # ----------
    # PROPERTIES
    # ----------

    # Property methods for boolean property
    def default_shutdown
      config_get_default('eigrp', 'shutdown')
    end

    def shutdown
      state = config_get('eigrp', 'shutdown', name: @name)
      state ? true : false
    end

    def shutdown=(state)
      state = (state ? '' : 'no')
      config_set('eigrp', 'shutdown', name: @name, state: state)
    end

    # Property methods for integer property
    def default_maximum_paths
      config_get_default('eigrp', 'maximum_paths')
    end

    def maximum_paths
      val = config_get('eigrp', 'maximum_paths', name: @name)
      val.nil? ? default_maximum_paths : val.first.to_i
    end

    def maximum_paths=(val)
      config_set('eigrp', 'maximum_paths', name: @name, val: val)
    end
  end
end
  • Use template-test_router.rb to build the minitest for router_eigrp.rb:
cp  docs/template-test_router.rb  tests/test_router_eigrp.rb
  • As with the API code, edit test_router_eigrp.rb and change the placeholder names as shown:
/X__CLASS_NAME__X/RouterEigrp/

/X__RESOURCE_NAME__X/eigrp/

/X__PROPERTY_BOOL__X/shutdown/

/X__PROPERTY_INT__X/maximum_paths/
  • At a minimum, the tests should include coverage for:
    • creating & destroying a single router eigrp instance
    • creating & destroying multiple router eigrp instances
    • feature disablement when removing last router eigrp
    • testing each property state

Example: test_router_eigrp.rb

This is the completed test_router_eigrp minitest based on template-test_router.rb:

# Copyright (c) 2014-2015 Cisco and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require_relative 'ciscotest'
require_relative '../lib/cisco_node_utils/router_eigrp'

# TestRouterEigrp - Minitest for RouterEigrp node utility class
class TestRouterEigrp < CiscoTestCase
  def setup
    # setup runs at the beginning of each test
    super
    no_feature_eigrp
  end

  def teardown
    # teardown runs at the end of each test
    no_feature_eigrp
    super
  end

  def no_feature_eigrp
    # Turn the feature off for a clean test.
    config('no feature eigrp')
  end

  # TESTS

  def test_router_create_destroy_one
    id = 'blue'
    rtr = RouterEigrp.new(id)
    @default_show_command = "show runn | i 'router eigrp #{id}'")
    assert_show_match(pattern: /^router eigrp #{id}$/,
                      msg:     "failed to create router eigrp #{id}")

    rtr.destroy
    refute_show_match(pattern: /^router eigrp #{id}$/,
                      msg:     "failed to destroy router eigrp #{id}")

    refute_show_match(command: "show runn | i 'feature eigrp'",
                      pattern: /^feature eigrp$/,
                      msg:     "failed to disable feature eigrp")
  end

  def test_router_create_destroy_multiple
    id1 = 'blue'
    rtr1 = RouterEigrp.new(id1)
    id2 = 'red'
    rtr2 = RouterEigrp.new(id2)

    @default_show_command = "show runn | i 'router eigrp'"

    s = @device.cmd("show runn | i 'router eigrp'")
    assert_match(s, /^router eigrp #{id1}$/)
    assert_match(s, /^router eigrp #{id2}$/)

    rtr1.destroy
    refute_show_match(pattern: /^router eigrp #{id1}$/,
                      msg:     "failed to destroy router eigrp #{id1}")

    rtr2.destroy
    refute_show_match(pattern: /^router eigrp #{id2}$/,
                      msg:     "failed to destroy router eigrp #{id2}")

    refute_show_match(command: "show runn | i 'feature eigrp'",
                      pattern: /^feature eigrp$/,
                      msg:     "failed to disable feature eigrp")
  end

  def test_router_maximum_paths
    id = 'blue'
    rtr = RouterEigrp.new(id)
    val = 5 # This value depends on property bounds
    rtr.maximum_paths = val
    assert_equal(rtr.maximum_paths, val, "maximum_paths is not #{val}")

    # Get default value from yaml
    val = node.config_get_default('eigrp', 'maximum_paths')
    rtr.maximum_paths = val
    assert_equal(rtr.maximum_paths, val, "maximum_paths is not #{val}")
  end

  def test_router_shutdown
    id = 'blue'
    rtr = RouterEigrp.new(id)
    rtr.shutdown = true
    assert(rtr.shutdown, 'shutdown state is not true')

    rtr.shutdown = false
    refute(rtr.shutdown, 'shutdown state is not false')
  end
end

Now run the test:

% ruby-1.9.3-p0 test_router_eigrp.rb -v -- 192.168.0.1 admin admin
Run options: -v -- --seed 56593

# Running tests:

CiscoTestCase#test_placeholder =
Ruby Version - 1.9.3
Node in CiscoTestCase Class: 192.168.0.1
Platform:
  - name  - my_n3k
  - type  - N3K-C3132Q-40GX
  - image -

2.90 s = .
TestCase#test_placeholder = 0.92 s = .
TestRouterEigrp#test_placeholder = 0.97 s = .
TestRouterEigrp#test_router_create_destroy_multiple = 10.77 s = .
TestRouterEigrp#test_router_create_destroy_one = 6.14 s = .
TestRouterEigrp#test_router_maximum_paths = 9.41 s = .
TestRouterEigrp#test_router_shutdown = 6.40 s = .


Finished tests in 37.512356s, 0.1866 tests/s, 0.3199 assertions/s.

7 tests, 12 assertions, 0 failures, 0 errors, 0 skips

rubocop is a Ruby static analysis tool. Run rubocop to validate the new code:

% rubocop lib/cisco_node_utils/router_eigrp.rb tests/test_router_eigrp.rb
Inspecting 2 file
..

2 file inspected, no offenses detected

The final step is to build and install the gem that contains the new APIs.

Please note: gem build will only include files that are part of the repository. This means that new file router_eigrp.rb will be ignored by the build until it is added to the repo with git add:

git add lib/cisco_node_utils/router_eigrp.rb

From the root of the cisco-network-node-utils repository issue the following command.

% gem build cisco_node_utils.gemspec
  Successfully built RubyGem
  Name: cisco_node_utils
  Version: 1.0.1
  File: cisco_node_utils-1.0.1.gem

Copy the new gem to your NX-OS device and then install it.

n9k#gem install --local /bootflash/cisco_node_utils-1.0.1.gem
Successfully installed cisco_node_utils-1.0.1
Parsing documentation for cisco_node_utils-1.0.1
Installing ri documentation for cisco_node_utils-1.0.1
Done installing documentation for cisco_node_utils after 2 seconds
1 gem installed

Conclusion

This was hopefully a good introduction to writing a Cisco node_utils API. At this point you could continue adding properties or try your hand at writing Puppet or Chef provider code to utilize your new API.