diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d15e998 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +.ruby-version +*.gem +coverage diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..8c18f1a --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--format documentation +--color diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..f921974 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,8 @@ +LineLength: + Enabled: false + +Style/ClassAndModuleChildren: + Enabled: false + +Documentation: + Enabled: false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..60cd0f7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +sudo: false +language: ruby +rvm: + - 1.9.3-p551 + - 2.0.0-p648 + - 2.1.9 + - 2.2.5 + - 2.3.1 +matrix: + allow_failures: + - rvm: 1.9.3-p551 + - rvm: 2.0.0-p648 + fast_finish: true +before_install: gem update --remote bundler +install: + - bundle install --retry=3 +script: + - bundle exec rake build diff --git a/CHANGELOG.md b/CHANGELOG.md index 47bb9ba..17d3423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,69 +1,103 @@ # 2013-April 0.0.10: 2013-04-26 - + Initial release of code into RubyGems. Code tested on EX and SRX-branch. Consider this code as "early-adopter". Comments/feedback is welcome and appreciated. 0.0.11: 2013-04-26 - + Updated Junos::Ez::RE::Utils #memory changed :procs from Hash to Array #users changed return from Hash to Array - + 0.0.12: 2013-04-26 - + Updated Junos::Ez::FS:Utils#ls to include :symlink information Adding Junos::Ez::Users::Provider for login management Adding Junos::Ez::UserAuths::Provider for SSH-key management - + 0.0.14: 2013-04-28 - + Completed initial documentation. Still more work to be done with these files but the "enough to get started" information is now available - + # 2013-May 0.0.15: 2013-05-02 - + L2ports - added support for framework to read [edit vlans] stanza to recognize interfaces configured there vs. under [edit interfaces] - + IPports - added :acl_in and :acl_out for stateless ACL filtering. added .status method to return runtime status information about the port - + RE::Utils - misc updates, and documentation 0.0.16: 2013-05-03 - + RE::Utils - added support for license-key management. Renamed "software" methods from "xxx_software" to "software_xxx" to be consistent with other naming usage. Updated docs. - + 0.0.17: 2013-05-05 - + FS::Utils - updated docs. fixed methods so that all "error" scenarios raise IOError excaptions. - + 0.1.0: 2013-05-06 - + All docs and code _finished_ for the inital release of code. Always more to do, but at this point, declaring the framework "good for early adopter testing". Looking forward to bug-reports, please open issues against this repo. Thank you! - + 0.1.1: 2013-05-29 - + Fixed a small bug in fact gathering for hardwaremodel - + # 2013-July 0.1.2: 2013-07-04 - + Fixed issue#3. Previously this gem would not work with non-VC capable EX switches. Updated the `facts/version.rb` file to handle these devices. Also added a new fact `:vc_capable` that is set to `true` if the EX can support virtual-chassis, and `false` if it cannot. # 2013-Aug - 0.2.0: *in progres* - + 0.2.0: + Fixed issue #6. Added support for EX4300 platform. Added new provider for Link Aggregation Group resources (LAGports) + +# 2016-March + + 1.0.0: + + Fixed issues + Issue #17 Add support for OCX device. + Issue #20 "under development" error is thrown while importing the interface_create recipe from the Chef-Server. + Issue #22 "netdev_vlan" resource action delete is not working fine while invoking from the JUNOS Chef-Client. + Issue #23 RPC command error: commit-configuration is getting thrown on Invoking the "netdev_lag" resource from + JUNOS Chef Client. + Issue #27 Duplicate declaration of lag configuration in a recipe is giving NoMethodError: undefined method + `properties' for nil:NilClass. + Issue #30 Error in rerunning netdev_lag interface. + Issue #33 undefined method `properties' for nil:NilClass error is thrown if the backup RE is unreachable. + Issue #35 Error in running chef client from Backup RE. + Issue #39 Getting 'Junos::Ez::NoProviderError' error on qfx device. + Issue #42 Raise exception to handle warnings in . + + Enhancement + * Add support for configuring l2_interface on MX device. + * Add support for provider 'group' for configuring JUNOS groups. + +# 2016-August + + 1.0.3 + Fixed issues + Issue #46 Removing references to rake from gemspec + Issue #47 cannot get the git source code + + Enhancement + * Valid project metadata and rake for cleanly building/releasing + * Test hooks and badges + * netconf gem pessimistic version constraint to 0.3.1 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..e7fe96b --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gemspec + +gem 'simplecov', require: false, group: :test diff --git a/README.md b/README.md index 77d4f7c..133a020 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ +[![Gem Version](https://badge.fury.io/rb/junos-ez-stdlib.svg)](https://badge.fury.io/rb/junos-ez-stdlib)[![Dependency Status](https://gemnasium.com/badges/github.com/Juniper/ruby-junos-ez-stdlib.svg)](https://gemnasium.com/github.com/Juniper/ruby-junos-ez-stdlib) +[![Build Status](https://travis-ci.org/Juniper/ruby-junos-ez-stdlib.svg?branch=master)](https://travis-ci.org/Juniper/ruby-junos-ez-stdlib) + # OVERVIEW Ruby framework to support Junos OS based device management automation. This is the "standard library" or "core" set of functionality that should work on most/all Junos OS based devices. -This framework is build on top of the NETCONF gem which uses XML as the fundamental data-exchange. So no -"automating the CLI" or using SNMP. The purpose of this framework is to **enable automation development +This framework is build on top of the NETCONF gem which uses XML as the fundamental data-exchange. So no +"automating the CLI" or using SNMP. The purpose of this framework is to **enable automation development without requiring specific Junos XML knowledge**. Further documentation can be found in the *docs* subdirectory. @@ -14,32 +17,32 @@ Further documentation can be found in the *docs* subdirectory. The framework is comprised of these basic eloements: - - Facts: + - Facts: A Hash of name/value pairs of information auto-collected. Fact values can be Hash structures as well so you can have deeply nested fact data. You can also define your own facts in addition to the "stdlib" facts. The facts are used by the framework to create a platform indepent layer of abstraction. This means that managing a VLAN, for example, is the same regardless of the underlying hardware platofrm (EX, QFX, MX, SRX, ...) - - - Resources: - Resources allow you to easily configure and perform operational functions on specific items within Junos, + - Resources: + + Resources allow you to easily configure and perform operational functions on specific items within Junos, for example VLANs, or switch ports. A resource has *properties* that you manipuate as Hash. You can - interact with Junos using resource methods like `read!`, `write!`, `delete!`, `activate!`, `deactivate!`, etc. + interact with Junos using resource methods like `read!`, `write!`, `delete!`, `activate!`, `deactivate!`, etc. For a complete listing of resource methods, refer to the *docs* directory - + - Providers: Providers allow you to manage a collection of resource, and most commonly, select a resource. The purpose of a provider/resource is to automate the life-cycle of common changes, like adding - VLANs, or ports to a VLAN. A provider also allows you to obtain a `list` of resources - (Array of *names*) or a `catalog` (Hash of resource properties). Providers may include resource - specific functionality, like using complex YAML/Hash data for easy import/export and provisioning + VLANs, or ports to a VLAN. A provider also allows you to obtain a `list` of resources + (Array of *names*) or a `catalog` (Hash of resource properties). Providers may include resource + specific functionality, like using complex YAML/Hash data for easy import/export and provisioning with Junos. If you need the ability to simply apply config-snippets that you do not need to model - as resources (as you might for initial device commissioning), the Utilities library is where you + as resources (as you might for initial device commissioning), the Utilities library is where you want to start. - + - Utilities: Utilities are simply collections of functions. The **configuration** utilities, for example, will @@ -47,9 +50,9 @@ The framework is comprised of these basic eloements: for unmanaged provider/resources (like initial configuration of the device). The **routing-engine** utilities, for example, will allow you to easily upgrade software, check memory usage, and do `ping` operations. - + # EXAMPLE USAGE - + ```ruby require 'pp' require 'net/netconf/jnpr' @@ -60,7 +63,7 @@ unless ARGV[0] exit 1 end -# login information for NETCONF session +# login information for NETCONF session login = { :target => ARGV[0], :username => 'jeremy', :password => 'jeremy1', } ## create a NETCONF object to manage the device and open the connection ... @@ -71,8 +74,8 @@ ndev.open puts "OK!" ## Now bind providers to the device object. The 'Junos::Ez::Provider' must be first. -## This will retrieve the device 'facts'. The other providers allow you to define the -## provider variables; so this example is using 'l1_ports' and 'ip_ports', but you could name +## This will retrieve the device 'facts'. The other providers allow you to define the +## provider variables; so this example is using 'l1_ports' and 'ip_ports', but you could name ## them what you like, yo! Junos::Ez::Provider( ndev ) @@ -125,7 +128,7 @@ ndev.cu.rollback! ndev.close ``` - + # PROVIDERS Providers manage access to individual resources and their associated properties. Providers/resources exists @@ -137,29 +140,30 @@ documentation on Providers/Resources, see the *docs* directory. - Vlans: VLAN resource management - IPports: IP v4 port management - StaticHosts: Static Hosts [system static-host-mapping ...] - - StaticRoutes: Static Routes [routing-options static ...] + - StaticRoutes: Static Routes [routing-options static ...] + - Group: JUNOS groups management # UTILITIES - Config: - + These functions allow you to load config snippets, do commit checks, look at config diffs, etc. - Generally speaking, you would want to use the Providers/Resources framework to manage specific + Generally speaking, you would want to use the Providers/Resources framework to manage specific items in the config. This utility library is very useful when doing the initial commissioning process, where you do not (cannot) model every aspect of Junos. These utilities can also be used in conjunction with Providers/Resources, specifically around locking/unlocking and committing the configuration. - + - Filesystem: - + These functions provide you "unix-like" commands that return data in Hash forms rather than as string output you'd normally have to screen-scraps. These methods include `ls`, `df`, `pwd`, `cwd`, `cleanup`, and `cleanup!` - Routing-Engine: - - These functions provide a general collection to information and functioanlity for handling - routing-engine (RE) processes. These functions `reboot!`, `shutdown!`, `install_software!`, + + These functions provide a general collection to information and functioanlity for handling + routing-engine (RE) processes. These functions `reboot!`, `shutdown!`, `install_software!`, `ping`. Information gathering such as memory-usage, current users, and RE status information is also made available through this collection. @@ -167,15 +171,21 @@ documentation on Providers/Resources, see the *docs* directory. * gem netconf * Junos OS based products - -# INSTALLATION + +# INSTALLATION * gem install junos-ez-stdlib # CONTRIBUTORS + Juniper Networks is actively contributing to and maintaining this repo. Please contact jnpr-community-netdev@juniper.net + for any queries. + + Contributors: + [John Deatherage](https://github.com/routelastresort), [Nitin Kumar](https://github.com/vnitinv), + [Priyal Jain](https://github.com/jainpriyal), [Ganesh Nalawade](https://github.com/ganeshrn) - * Jeremy Schulman, @nwkautomaniac - * Ganesh Nalawade, @ganesh634 + Former Contributors: + [Jeremy Schulman](https://github.com/jeremyschulman) # LICENSES diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..4c774a2 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' + +RSpec::Core::RakeTask.new(:spec) + +task default: :spec diff --git a/docs/Providers/Group.md b/docs/Providers/Group.md new file mode 100644 index 0000000..f187b25 --- /dev/null +++ b/docs/Providers/Group.md @@ -0,0 +1,61 @@ +# Junos::Ez::Group::Provider + +Manages JUNOS group properties + +# EXAMPLE + +The provider *name* selector is the JUNOS group name, e.g. "service_group". + +```ruby +Junos::Ez::Group::Provider( ndev, :group ) + +grp = ndev.group["service_group"] + +grp[:format] = 'set' +grp[:path] = 'services.set' + +grp.write! + +``` + +# PROPERTIES + + - `:format` - JUNOS configuration format is file. It can be 'xml', 'text' or 'set'. Default is 'xml' + - `:path` - Path of configuration file that is applied inside JUNOS group hierarchy. + +# METHODS + +No additional methods at this time ... + +# USAGE NOTES + +Contents of 'service.set' file + +```` +% cat services.set +set system services ftp +set system services ssh +set system services netconf ssh +```` + +JUNOS group configuration reflected on executing above example. + +```` +{master}[edit] +junos@switch# show groups service_group +system { + services { + ftp; + ssh; + netconf { + ssh; + } + } +} + +junos@switch# show apply-groups +apply-groups [ global re0 re1 service_group ]; + +```` + + diff --git a/lib/junos-ez/facts/chassis.rb b/lib/junos-ez/facts/chassis.rb index d9c2fef..d0e2b15 100644 --- a/lib/junos-ez/facts/chassis.rb +++ b/lib/junos-ez/facts/chassis.rb @@ -1,6 +1,12 @@ Junos::Ez::Facts::Keeper.define( :chassis ) do |ndev, facts| inv_info = ndev.rpc.get_chassis_inventory + errs = inv_info.xpath('//output')[0] + + if errs and errs.text.include? "This command can only be used on the master routing engine" + raise Junos::Ez::NoProviderError, "Chef can only be used on master routing engine !!" + end + chassis = inv_info.xpath('chassis') facts[:hardwaremodel] = chassis.xpath('description').text diff --git a/lib/junos-ez/facts/personality.rb b/lib/junos-ez/facts/personality.rb index 0b834ec..120b70b 100644 --- a/lib/junos-ez/facts/personality.rb +++ b/lib/junos-ez/facts/personality.rb @@ -6,19 +6,19 @@ examine = ( model != "Virtual Chassis" ) ? model : facts.select {|k,v| k.match(/^RE[0..9]+/) }.values[0][:model] facts[:personality] = case examine - when /^(EX)|(QFX)|(OCX)/ + when /^(EX)|(QFX)|(PTX)|(OCX)/i :SWITCH - when /^MX/ + when /^MX/i :MX - when /^vMX/ + when /^vMX/i facts[:virtual] = true :MX - when /SRX(\d){3}/ + when /SRX(\d){3}/i :SRX_BRANCH when /junosv-firefly/i facts[:virtual] = true :SRX_BRANCH - when /SRX(\d){4}/ + when /SRX(\d){4}/i :SRX_HIGHEND end diff --git a/lib/junos-ez/facts/switch_style.rb b/lib/junos-ez/facts/switch_style.rb index ad4f201..da98f53 100644 --- a/lib/junos-ez/facts/switch_style.rb +++ b/lib/junos-ez/facts/switch_style.rb @@ -11,7 +11,7 @@ :NONE when /^(ex9)|(ex43)|(ocx)/i :VLAN_L2NG - when /^(qfx5)|(qfx3)/i + when /^(qfx)/i if facts[:version][0..3].to_f >= 13.2 :VLAN_L2NG else diff --git a/lib/junos-ez/facts/version.rb b/lib/junos-ez/facts/version.rb index ad3dd38..d357e41 100644 --- a/lib/junos-ez/facts/version.rb +++ b/lib/junos-ez/facts/version.rb @@ -1,46 +1,59 @@ -Junos::Ez::Facts::Keeper.define( :version ) do |ndev, facts| - - f_master, f_persona = uses :master, :personality - - case f_persona - when :MX - swver = ndev.rpc.command "show version invoke-on all-routing-engines" - when :SWITCH - ## most EX switches support the virtual-chassis feature, so the 'all-members' option would be valid - ## in some products, this options is not valid (i.e. not vc-capable. so we're going to try for vc, and if that - ## throws an exception we'll rever to non-VC - - begin - swver = ndev.rpc.command "show version all-members" - rescue Netconf::RpcError - facts[:vc_capable] = false - swver = ndev.rpc.command "show version" - else - facts[:vc_capable] = true - end - else - swver = ndev.rpc.command "show version" - end - - if swver.name == 'multi-routing-engine-results' - swver_infos = swver.xpath('//software-information') - swver_infos.each do |re_sw| - re_name = re_sw.xpath('preceding-sibling::re-name').text.upcase - re_sw.xpath('package-information[1]/comment').text =~ /\[(.*)\]/ - ver_key = ('version_' + re_name).to_sym - facts[ver_key] = $1 - end - master_id = f_master - unless master_id.nil? - facts[:version] = - facts[("version_" + "RE" + master_id).to_sym] || - facts[("version_" + "LOCALRE").to_sym] || - facts[('version_' + "FPC" + master_id).to_sym] - end - else - junos = swver.xpath('//package-information[name = "junos"]/comment').text - junos =~ /\[(.*)\]/ - facts[:version] = $1 - end - -end +Junos::Ez::Facts::Keeper.define( :version ) do |ndev, facts| + + f_master, f_persona = uses :master, :personality + + case f_persona + when :MX + begin + swver = ndev.rpc.command "show version invoke-on all-routing-engines" + rescue Netconf::RpcError + swver = ndev.rpc.command "show version" + end + when :SWITCH + ## most EX switches support the virtual-chassis feature, so the 'all-members' option would be valid + ## in some products, this options is not valid (i.e. not vc-capable. so we're going to try for vc, and if that + ## throws an exception we'll rever to non-VC + + begin + swver = ndev.rpc.command "show version all-members" + rescue Netconf::RpcError + facts[:vc_capable] = false + swver = ndev.rpc.command "show version" + else + facts[:vc_capable] = true + end + else + swver = ndev.rpc.command "show version" + end + + if swver.name == 'multi-routing-engine-results' + swver_infos = swver.xpath('//software-information') + swver_infos.each do |re_sw| + re_name = re_sw.xpath('preceding-sibling::re-name').text.upcase + ver_key = ('version_' + re_name).to_sym + + if re_sw.at_xpath('//junos-version') + facts[ver_key] = re_sw.xpath('//junos-version').text + else + re_sw.xpath('package-information[1]/comment').text =~ /\[(.*)\]/ + facts[ver_key] = $1 + end + end + master_id = f_master + unless master_id.nil? + facts[:version] = + facts[("version_" + "RE" + master_id).to_sym] || + facts[("version_" + "LOCALRE").to_sym] || + facts[('version_' + "FPC" + master_id).to_sym] + end + else + if swver.at_xpath('//junos-version') + facts[:version] = swver.xpath('//junos-version').text + else + junos = swver.xpath('//package-information[name = "junos"]/comment').text + junos =~ /\[(.*)\]/ + facts[:version] = $1 + end + end + +end diff --git a/lib/junos-ez/group.rb b/lib/junos-ez/group.rb new file mode 100644 index 0000000..b4cbb2b --- /dev/null +++ b/lib/junos-ez/group.rb @@ -0,0 +1,206 @@ +require "junos-ez/provider" + +module Junos::Ez::Group + + PROPERTIES = [ + :format, # [:set, :text, :xml] + :path, # Configuration file path +] + + def self.Provider( ndev, varsym ) + newbie = Junos::Ez::Group::Provider::new( ndev ) + newbie.properties = Junos::Ez::Provider::PROPERTIES + PROPERTIES + Junos::Ez::Provider.attach_instance_variable( ndev, varsym, newbie ) + end + + class Provider < Junos::Ez::Provider::Parent + # common parenting goes here ... if we were to + # subclass the objects ... not doing that now + end + +end + +class Junos::Ez::Group::Provider + + ### --------------------------------------------------------------- + ### XML top placement + ### --------------------------------------------------------------- + + def xml_at_top + xml = Nokogiri::XML::Builder.new {|xml| xml.configuration { + xml.groups { + xml.name @name + return xml + } + }} + end + + ### --------------------------------------------------------------- + ### XML property readers + ### --------------------------------------------------------------- + + def xml_get_has_xml( xml ) + xml.xpath('//groups')[0] + end + + def xml_read_parser( as_xml, as_hash ) + set_has_status( as_xml, as_hash ) + + grp = as_xml.xpath('name').text + as_hash[:name] = grp unless grp.empty? + + end + + + ### --------------------------------------------------------------- + ### XML property writers + ### --------------------------------------------------------------- + + def xml_change_path( xml ) + end + + def xml_change_format( xml ) + end + + ### --------------------------------------------------------------- + ### XML on-create + ### --------------------------------------------------------------- + + def xml_on_create( xml ) + end + + ### --------------------------------------------------------------- + ### XML on-delete + ### --------------------------------------------------------------- + def xml_on_delete( xml ) + end + + def write_xml_config!( xml, opts = {} ) + if (@should[:_exist] == true) + _load ( xml ) + @should[:format] = 'xml' unless @should[:format] + begin + attr = {} + attr[:action] = 'replace' + attr[:format] = @should[:format].to_s + result = @ndev.rpc.load_configuration( @config.to_s, attr ) + rescue Netconf::RpcError => e + errs = e.rsp.xpath('//rpc-error[error-severity = "error"]') + raise e unless errs.empty? + e.rsp + else + result + end + else + super(xml) + end + _apply_group + end + + def write! + return nil if @should.empty? + + @should[:_exist] ||= true + @should[:_active] ||= :true + # load the conifguration from file and apply under group + # hirerachy + rsp = write_xml_config!( xml_at_top.doc.root ) + + # copy the 'should' values into the 'has' values now that + # they've been written back to Junos + + @has.merge! @should + @should.clear + + return true + end + +end + + +##### --------------------------------------------------------------- +##### Provider collection methods +##### --------------------------------------------------------------- + +class Junos::Ez::Group::Provider + + def build_list + grp_cfgs = @ndev.rpc.get_configuration{|xml| + xml.send(:'groups') + }.xpath('groups/name').collect do |item| + item.text + end + return grp_cfgs + end + + def build_catalog + return @catalog if list!.empty? + list.each do |grp_name| + @ndev.rpc.get_configuration{ |xml| + xml.groups { + xml.name grp_name + } + }.xpath('groups').each do |as_xml| + @catalog[grp_name] = {} + xml_read_parser( as_xml, @catalog[grp_name] ) + end + end + @catalog + end +end + +##### --------------------------------------------------------------- +##### _PRIVATE methods +##### --------------------------------------------------------------- + +class Junos::Ez::Group::Provider + + def _load ( xml ) + return @config = nil if ( @should[:_exist] == false ) + admin = '' + if @should[:format].to_s == 'set' + @config = "\ndelete groups #{@name}\n" + + "edit groups #{@name}\n" + + File.read( @should[:path] ) + admin = @should[:_active] == :false ? 'deactivate' : 'activate' + @config += "\nquit\n" + @config += "\n#{admin} groups #{@name}" + + elsif @should[:format].to_s == 'text' + admin = @should[:_active] == :false ? 'inactive' : 'active' + admin += ": " unless admin.empty? + @config = "groups {\n#{admin} replace: #{@name} {\n" + + File.read( @should[:path] ) + "\n}\n}" + + elsif @should[:format].to_s == 'xml' + xml.at_xpath('groups') << File.read( @should[:path]) + @config = xml + end + return @config + end + + def _apply_group + cfg = Netconf::JunosConfig.new(:TOP) + xml = cfg.doc + Nokogiri::XML::Builder.with( xml.at_xpath( 'configuration' )) do |dot| + if @config and @should[:_active] == :true + dot.send :'apply-groups', @name + else + dot.send :'apply-groups', @name, Netconf::JunosConfig::DELETE + end + end + begin + attr = {} + attr[:action] = 'replace' + attr[:format] = 'xml' + result = @ndev.rpc.load_configuration( xml, attr ) + rescue Netconf::RpcError => e + errs = e.rsp.xpath('//rpc-error[error-severity = "error"]') + raise e unless errs.empty? + e.rsp + else + result + end + end +end + diff --git a/lib/junos-ez/l2_ports.rb b/lib/junos-ez/l2_ports.rb index 21f8fbe..c94ea66 100644 --- a/lib/junos-ez/l2_ports.rb +++ b/lib/junos-ez/l2_ports.rb @@ -18,7 +18,8 @@ def self.Provider( ndev, varsym ) when :VLAN_L2NG Junos::Ez::L2ports::Provider::VLAN_L2NG.new( ndev ) when :BRIDGE_DOMAIN - raise ArgumentError, "under development" + Junos::Ez::L2ports::Provider::BRIDGE_DOMAIN.new(ndev) + #raise ArgumentError, "under development" # Junos::Ez::L2ports::Provider::BRIDGE_DOMAIN.new( ndev ) end @@ -49,7 +50,7 @@ def mode_changed? ### --------------------------------------------------------------- def xml_change__active( xml ) - par = xml.instance_variable_get(:@parent).at_xpath('ancestor::unit') + par = xml.instance_variable_get(:@parent).at_xpath('ancestor::interface') value = @should[:_active] ? 'active' : 'inactive' par[value] = value # attribute name is same as value end @@ -60,7 +61,6 @@ def xml_change__active( xml ) require 'junos-ez/l2_ports/vlan' require 'junos-ez/l2_ports/vlan_l2ng' - -# require 'junos-ez/l2ports/bridge_domain' ... under development +require 'junos-ez/l2_ports/bridge_domain' diff --git a/lib/junos-ez/l2_ports/bridge_domain.rb b/lib/junos-ez/l2_ports/bridge_domain.rb index e69de29..9f90f19 100644 --- a/lib/junos-ez/l2_ports/bridge_domain.rb +++ b/lib/junos-ez/l2_ports/bridge_domain.rb @@ -0,0 +1,499 @@ +class Junos::Ez::L2ports::Provider::BRIDGE_DOMAIN< Junos::Ez::L2ports::Provider + + ### --------------------------------------------------------------- + ### XML top placement + ### --------------------------------------------------------------- + + def xml_at_top + Nokogiri::XML::Builder.new {|xml| xml.configuration { + xml.interfaces { + return xml_at_element_top( xml, @name ) + } + }} + end + + # set the edit anchor inside bridge-domains stanza + # we will need to 'up-out' when making changes to the + # unit information, like description + + def xml_at_element_top( xml, name ) + xml.interface { + xml.name name + xml.send(:'native-vlan-id') + xml.unit { + xml.name '0' + return xml + } + } + end + + ### --------------------------------------------------------------- + ### XML property readers + ### --------------------------------------------------------------- + + def xml_get_has_xml( xml ) + # second unit contains the family/bridge-domains stanza + got = xml.xpath('//unit')[0] + # if this resource doesn't exist we need to default some + # values into has/should variables + unless got + @has[:vlan_tagging] = false + @should = @has.clone + end + got + end + + def xml_read_parser( as_xml, as_hash ) + ## reading is anchored at the [... unit 0 ...] level + set_has_status( as_xml, as_hash ) + + xml_when_item(as_xml.xpath('description')){|i| as_hash[:description] = i.text} + + f_eth = as_xml.xpath('family/bridge') + as_hash[:vlan_tagging] = f_eth.xpath('interface-mode').text.chomp == 'trunk' + + # obtain a copy of the running state, this is needed in case the config + # is located under the [edit vlans] stanza vs. [edit interfaces] + + ifs_name = @name || as_xml.xpath('ancestor::interface/name').text.strip + eth_port_vlans = _get_eth_port_vlans_h( ifs_name ) + @under_vlans = [] + + # --- access port + + if as_hash[:vlan_tagging] == false + xml_when_item(f_eth.xpath('domain/vlan-id')){ |i| as_hash[:untagged_vlan] = i.text.chomp } + unless as_hash[:untagged_vlan] + as_hash[:untagged_vlan] = eth_port_vlans[:untagged] + @under_vlans << eth_port_vlans[:untagged] + end + return + end + + # --- trunk port + as_hash[:untagged_vlan] ||= eth_port_vlans[:untagged] + as_hash[:tagged_vlans] = f_eth.xpath('//bridge/vlan-id-list').collect { |v| v.text.chomp }.to_set + (eth_port_vlans[:tagged] - as_hash[:tagged_vlans]).each do |vlan| + as_hash[:tagged_vlans] << vlan + @under_vlans << vlan + end + # native-vlan-id is set at the interface level, and is the VLAN-ID, not the vlan + # name. So we need to do a bit of translating here. The *ASSUMPTION* is that the + # native-vlan-id value is a given VLAN in the tagged_vlan list. So we will use + # that list to do the reverse lookup on the tag-id => name + as_hash[:tagged_vlans]= as_hash[:tagged_vlans].collect {|x| _vlan_tag_id_to_name(x)} + xml_when_item(f_eth.xpath('ancestor::interface/native-vlan-id')){ |i| + as_hash[:untagged_vlan] = _vlan_tag_id_to_name( i.text.chomp) + } + as_hash[:tagged_vlans].delete(as_hash[:untagged_vlan]) + end + + ### --------------------------------------------------------------- + ### XML on_create, on_delete handlers + ### --------------------------------------------------------------- + + ## overload the xml_on_delete method since we may need + ## to do some cleanup work in the [edit vlans] stanza + + def xml_on_delete( xml ) + @ifd = xml.instance_variable_get(:@parent).at_xpath('ancestor::interface') + @ifd.xpath('//native-vlan-id').remove ## remove the element from the get-config + ## need to add check if any native-vlan-id is present or not (untagged vlan)##### + if is_trunk? and @ifd.xpath('//native-vlan-id') + _delete_native_vlan_id( xml ) + end + + return unless @under_vlans + return if @under_vlans.empty? + + _xml_rm_under_vlans( xml, @under_vlans ) + end + + ### --------------------------------------------------------------- + ### XML property writers + ### --------------------------------------------------------------- + + def xml_at_here( xml ) + @ifd = xml.instance_variable_get(:@parent).at_xpath('ancestor::interface') + @ifd.xpath('//native-vlan-id').remove ## remove the element from the get-config + xml.family { + xml.send(:'bridge') { + return xml + } + } + end + + def xml_build_change( nop = nil ) + @under_vlans ||= [] # handles case for create'd port + if mode_changed? + @should[:untagged_vlan] ||= @has[:untagged_vlan] + end + super xml_at_here( xml_at_top ) + end + + ## ---------------------------------------------------------------- + ## :description + ## ---------------------------------------------------------------- + + ## overload default method since we need to "up-out" of the + + def xml_change_description( xml ) + unit = xml.parent.xpath('ancestor::unit')[0] + Nokogiri::XML::Builder.with( unit ){ |x| + xml_set_or_delete( x, 'description', @should[:description] ) + } + end + + ## ---------------------------------------------------------------- + ## :vlan_tagging + ## ---------------------------------------------------------------- + + def xml_change_vlan_tagging( xml ) + port_mode = should_trunk? ? 'trunk' : 'access' + xml.send(:'interface-mode', port_mode ) + + # when the vlan_tagging value changes then this method + # will trigger updates to the untagged_vlan and tagged_vlans + # resource values as well. + # !!! DO NOT SWAP THIS ORDER untagged processing *MUST* BE FIRST! + + upd_untagged_vlan( xml ) + upd_tagged_vlans( xml ) + + return true + end + + def set_ifd_trunking( xml, should_trunk ) + par = xml.instance_variable_get(:@parent) + Nokogiri::XML::Builder.with( par.at_xpath( 'ancestor::interface' )) do |dot| + if should_trunk + dot.send( :'flexible-vlan-tagging' ) + dot.send( :'encapsulation', 'flexible-ethernet-services' ) + else + dot.send( :'flexible-vlan-tagging', Netconf::JunosConfig::DELETE ) + dot.send( :'encapsulation', Netconf::JunosConfig::DELETE ) + end + end +end + + ## ---------------------------------------------------------------- + ## :tagged_vlans + ## ---------------------------------------------------------------- + + def xml_change_tagged_vlans( xml ) + return false if mode_changed? + upd_tagged_vlans( xml ) + end + + def upd_tagged_vlans( xml ) + return false unless should_trunk? + + @should[:tagged_vlans] = @should[:tagged_vlans].to_set if @should[:tagged_vlans].kind_of? Array + @has[:tagged_vlans] = @has[:tagged_vlans].to_set if @has[:tagged_vlans].kind_of? Array + + v_should = @should[:tagged_vlans] || Set.new + v_has = @has[:tagged_vlans] || Set.new + + del = v_has - v_should + add = v_should - v_has + + del_under_vlans = del & @under_vlans + unless del_under_vlans.empty? + del = del ^ @under_vlans + _xml_rm_under_vlans( xml, del_under_vlans ) + @under_vlans = [] + end + + if add or del + del.each{|v| xml.send(:'vlan-id-list', _vlan_name_to_tag_id( v ), Netconf::JunosConfig::DELETE)} + add.each{|v| xml.send( :'vlan-id-list', _vlan_name_to_tag_id(v) )} + end + return true + end + + ## ---------------------------------------------------------------- + ## :untagged_vlan + ## ---------------------------------------------------------------- + + def xml_change_untagged_vlan( xml ) + return false if mode_changed? + upd_untagged_vlan( xml ) + end + + def upd_untagged_vlan( xml ) + self.class.change_untagged_vlan( self, xml ) + end + +end + +##### --------------------------------------------------------------- +##### Class methods for handling state-transitions between +##### configurations (tagged/untagged) +##### --------------------------------------------------------------- + +class Junos::Ez::L2ports::Provider::BRIDGE_DOMAIN + + # creating some class definitions ... + # this is a bit complicated because we need to handle port-mode + # change transitions; basically dealing with the fact that + # trunk ports use 'native-vlan-id' and access ports have a + # vlan member definition; i.e. they don't use native-vlan-id, ugh. + # Rather than doing all this logic as if/then/else statements, + # I've opted to using a proc jump-table technique. Lessons + # learned from lots of embedded systems programming :-) + + def self.init_jump_table + + # auto-hash table, majik! + hash = Hash.new(&(p=lambda{|h,k| h[k] = Hash.new(&p)})) + + # ------------------------------------------------------------------ + # - jump table for handling various untagged vlan change use-cases + # ------------------------------------------------------------------ + # There are three criteria for selection: + # | is_trunk | will_trunk | no_untg | + # ------------------------------------------------------------------ + + # - will not have untagged vlan + hash[false][false][true] = self.method(:ac_ac_nountg) + hash[false][true][true] = self.method(:ac_tr_nountg) + hash[true][false][true] = self.method(:tr_ac_nountg) + hash[true][true][true] = self.method(:tr_tr_nountg) + + # - will have untagged vlan + hash[false][false][false] = self.method(:ac_ac_untg) + hash[false][true][false] = self.method(:ac_tr_untg) + hash[true][false][false] = self.method(:tr_ac_untg) + hash[true][true][false] = self.method(:tr_tr_untg) + + hash + end + + ### invoke the correct method from the jump table + ### based on the three criteria to select the action + + def self.change_untagged_vlan( this, xml ) + @@ez_l2_jmptbl ||= init_jump_table + proc = @@ez_l2_jmptbl[this.is_trunk?][this.should_trunk?][this.should[:untagged_vlan].nil?] + proc.call( this, xml ) + end + + ### ------------------------------------------------------------- + ### The following are all the change transition functions for + ### each of the use-cases + ### ------------------------------------------------------------- + + def self.ac_ac_nountg( this, xml ) + #NetdevJunos::Log.debug "ac_ac_nountg" + # @@@ a port *MUST* be assigned to a vlan in access mode on MX. + # @@@ generate an error! + raise Junos::Ez::NoProviderError, "a port *MUST* be assigned to a vlan in access mode on MX." + end + + def self.ac_tr_nountg( this, xml ) + #no action needed handled already + end + + def self.tr_ac_nountg( this, xml ) + # @@@ a port *MUST* be assigned to a vlan in access mode on MX. + # @@@ generate an error! + raise Junos::Ez::NoProviderError, "a port *MUST* be assigned to vlan in access mode on MX" + end + + def self.tr_tr_nountg( this, xml ) + this._delete_native_vlan_id( xml ) + end + + ## ---------------------------------------------------------------- + ## transition where port WILL-HAVE untagged-vlan + ## ---------------------------------------------------------------- + + def self.ac_ac_untg( this, xml ) + vlan_id = this._vlan_name_to_tag_id( this.should[:untagged_vlan] ) + xml.send :'vlan-id', vlan_id + end + + def self.ac_tr_untg( this, xml ) + was_untg_vlan = this.has[:untagged_vlan] + this._set_native_vlan_id( xml, this.should[:untagged_vlan] ) + this._xml_rm_ac_untagged_vlan( xml ) if was_untg_vlan + end + + def self.tr_ac_untg( this, xml ) + this._delete_native_vlan_id( xml ) + vlan_id = this._vlan_name_to_tag_id( this.should[:untagged_vlan] ) + xml.send( :'vlan-id', vlan_id ) + end + + def self.tr_tr_untg( this, xml ) + this._set_native_vlan_id(xml, this.should[:untagged_vlan]) + end + +end + +##### --------------------------------------------------------------- +##### Provider collection methods +##### --------------------------------------------------------------- + +class Junos::Ez::L2ports::Provider::BRIDGE_DOMAIN + + def build_list + begin + got = @ndev.rpc.get_bridge_instance_information( :brief => true) + rescue => e + # in this case, no ethernet-switching is enabled so return empty list + return [] + end + got.xpath('//l2iff-interface-name').collect{ |ifn| ifn.text.split('.')[0] } + end + + def build_catalog + @catalog = {} + return @catalog if list!.empty? + list.each do |ifs_name| + @ndev.rpc.get_configuration{ |xml| + xml.interfaces { + xml_at_element_top( xml, ifs_name ) + } + }.xpath('interfaces/interface').each do |ifs_xml| + @catalog[ifs_name] = {} + unit = xml_get_has_xml( ifs_xml ) + xml_read_parser( unit, @catalog[ifs_name] ) + end + end + + @catalog + end + +end + +##### --------------------------------------------------------------- +##### !!!!! PRIVATE METHODS !!!! +##### --------------------------------------------------------------- + +class Junos::Ez::L2ports::Provider::BRIDGE_DOMAIN + private + + def _get_eth_port_vlans_h( ifs_name ) + got = @ndev.rpc.get_bridge_instance_information(:interface => ifs_name) + ret_h = {:untagged => nil, :tagged => Set.new } + got.xpath('//l2ng-l2ald-iff-interface-entry').each do |vlan| + # one of the node-set elements (the first one?) contains the interface name. + # this doesn't have any VLAN information, so skip it. + next if vlan.xpath('l2iff-interface-name') + + vlan_name = vlan.xpath('//l2rtb-bridge-vlan').text.strip + if vlan.xpath('//l2rtb-interface-vlan-member-tagness') + tgdy = vlan.xpath('//l2rtb-interface-vlan-member-tagness').text.strip + if tgdy == 'untagged' + ret_h[:untagged] = vlan_name + else + ret_h[:tagged] << vlan_name + end + else + ret_h[:tagged]< 'committed'} + @ndev.rpc.get_configuration(xml_at_top, database) + end def xml_get_has_xml( xml ) - @ifd_ether_options = 'ether-options' # @@@ hack for now - + if ndev.facts[:ifd_style] == "CLASSIC" + @ifd_ether_options = 'gigether-options' + else + @ifd_ether_options = 'ether-options' + end xml.xpath('//interface')[0] end @@ -53,7 +79,7 @@ def xml_read_parser( as_xml, as_hash ) # property :links ae_name = as_xml.xpath('name').text - as_hash[:links] = Set.new(_get_port_list( ae_name )) + as_hash[:links] = Set.new(get_cookie_links(as_xml)) # property :lacp ae_opts = as_xml.xpath('aggregated-ether-options') @@ -70,14 +96,34 @@ def xml_read_parser( as_xml, as_hash ) ### --------------------------------------------------------------- ### XML property writers ### --------------------------------------------------------------- - + def update_ifd_should() + if @should[:links].empty? + raise Junos::Ez::NoProviderError, "\n *links* are compulsory for creating lag interface!!! \n" + else + ether_option = @should[:links][0].to_s + @ifd_ether_options = (ether_option.start_with? 'fe-') ? 'fastether-options' : 'gigether-options' + end + end + + def update_ifd_has() + @has[:links] = @has[:links].to_a + if @has[:links].empty? + raise Junos::Ez::NoProviderError, "\n Either lag interface is not created or links associated with given lag interface is not supported \n" + else + ether_option = @has[:links][0].to_s + @ifd_ether_options = (ether_option.start_with? 'fe-') ? 'fastether-options' : 'gigether-options' + end + end + def xml_change_links( xml ) - + update_ifd_should() @should[:links] = @should[:links].to_set if @should[:links].kind_of? Array has = @has[:links] || Set.new should = @should[:links] || Set.new + set_cookie_links( xml ) + del = has - should add = should - has @@ -144,7 +190,7 @@ def xml_on_create( xml ) ### --------------------------------------------------------------- def xml_on_delete( xml ) - + update_ifd_has() par = xml.instance_variable_get(:@parent) dot_ifd = par.at_xpath('ancestor::interfaces') diff --git a/lib/junos-ez/provider.rb b/lib/junos-ez/provider.rb index 5091ff7..7a11b79 100644 --- a/lib/junos-ez/provider.rb +++ b/lib/junos-ez/provider.rb @@ -544,7 +544,7 @@ def write_xml_config!( xml, opts = {} ) action = {'action' => 'replace' } result = @ndev.rpc.load_configuration( xml, action ) rescue Netconf::RpcError => e - errs = e.rsp.xpath('//rpc-error[error-severity = "error"]') + errs = e.rsp.xpath('//rpc-error') raise e unless errs.empty? e.rsp else diff --git a/lib/junos-ez/stdlib.rb b/lib/junos-ez/stdlib.rb index 3c1c2b1..8ecd286 100644 --- a/lib/junos-ez/stdlib.rb +++ b/lib/junos-ez/stdlib.rb @@ -7,6 +7,7 @@ require 'junos-ez/l2_ports' # switch ports require 'junos-ez/ip_ports' # ip ports (v4) require 'junos-ez/lag_ports' # Link Aggregation Groups +require 'junos-ez/group' # ------------------------------------------------------------------- # utility libraries, not providers diff --git a/lib/junos-ez/version.rb b/lib/junos-ez/version.rb index 7baac3c..a9b1b95 100644 --- a/lib/junos-ez/version.rb +++ b/lib/junos-ez/version.rb @@ -1,6 +1,6 @@ module Junos; end module Junos::Ez; end - + module Junos::Ez - VERSION = "0.2.0" + VERSION = '1.0.3'.freeze end diff --git a/lib/junos-ez/vlans/bridge_domain.rb b/lib/junos-ez/vlans/bridge_domain.rb index bf3d2fd..50ed830 100644 --- a/lib/junos-ez/vlans/bridge_domain.rb +++ b/lib/junos-ez/vlans/bridge_domain.rb @@ -21,9 +21,13 @@ def xml_read! return nil unless (@has_xml = cfg_xml.xpath('//domain')[0]) xml_read_parser( @has_xml, @has ) end + + def xml_get_has_xml( xml ) + xml.xpath('//domain')[0] + end def xml_read_parser( as_xml, as_hash ) - status_from_junos( as_xml, as_hash ) + set_has_status( as_xml, as_hash ) as_hash[:vlan_id] = as_xml.xpath('vlan-id').text.to_i as_hash[:description] = as_xml.xpath('description').text as_hash[:no_mac_learning] = as_xml.xpath('bridge-options/no-mac-learning').empty? ? :disable : :enable diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..39e075f --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,2 @@ +require 'simplecov' +SimpleCov.start