diff --git a/lib/junos-ez/facts.rb b/lib/junos-ez/facts.rb index ce1c670..9a91342 100644 --- a/lib/junos-ez/facts.rb +++ b/lib/junos-ez/facts.rb @@ -1,83 +1,97 @@ -require 'junos-ez/provider' - -### ----------------------------------------------------------------- -### Junos::Ez module devices the toplevel Provider and associated -### Facts class & methods -### ----------------------------------------------------------------- - -module Junos::Ez - - attr_accessor :providers, :facts - - def self.Provider( ndev ) - ndev.extend Junos::Ez - ndev.providers = [] - ndev.facts = Junos::Ez::Facts::Keeper.new( ndev ) - ndev.facts.read! - true - end - - def fact( name ); facts[name] end -end; - -module Junos::Ez::Facts - - class Keeper - attr_accessor :known - - def initialize( ndev ) - @ndev = ndev - @known = Hash.new - end - - def clear; @known.clear end - - def list; @known.keys end - def list!; read!; list; end - - def catalog; @known end - def catalog!; read!; catalog end - - def uses( *facts ) - values = facts.collect do |f| - self.send( "fact_read_#{f}", @ndev, @known ) unless @known[f] - self[f] - end - (values.count == 1) ? values[0] : values - end - - def self.define( fact, &block ) - define_method( "fact_read_#{fact}".to_sym, block ) - end - - def []=(key,value) - @known[key] = value - end - - def [](key) - @known[key] - end - - def read! - @known.clear - fact_readers = self.methods.grep /^fact_read_/ - fact_readers.each do |getter| - getter =~ /^fact_read_(\w+)/ - fact = $1.to_sym - self.send( getter, @ndev, @known ) unless @known[fact] - end - end - - end # class -end - -### ----------------------------------------------------------------- -### Load all of the fact files -### ----------------------------------------------------------------- - -require 'junos-ez/facts/chassis' -require 'junos-ez/facts/personality' -require 'junos-ez/facts/version' -require 'junos-ez/facts/switch_style' -require 'junos-ez/facts/ifd_style' - +require 'junos-ez/provider' + +### ----------------------------------------------------------------- +### Junos::Ez module devices the toplevel Provider and associated +### Facts class & methods +### ----------------------------------------------------------------- + +module Junos::Ez + + attr_accessor :providers, :facts + + def self.Provider( ndev ) + ndev.extend Junos::Ez + ndev.providers = [] + ndev.facts = Junos::Ez::Facts::Keeper.new( ndev ) + ndev.facts.read! + true + end + + def fact( name ); facts[name] end +end; + +module Junos::Ez::Facts + + class Keeper + attr_accessor :known + + def initialize( ndev ) + @ndev = ndev + @known = Hash.new + end + + def clear; @known.clear end + + def list; @known.keys end + def list!; read!; list; end + + def catalog; @known end + def catalog!; read!; catalog end + + def uses( *facts ) + values = facts.collect do |f| + self.send( "fact_read_#{f}", @ndev, @known ) unless @known[f] + self[f] + end + (values.count == 1) ? values[0] : values + end + + def self.define( fact, &block ) + define_method( "fact_read_#{fact}".to_sym, block ) + end + + def []=(key,value) + @known[key] = value + end + + def [](key) + @known[key] + end + + def read! + @known.clear + fact_readers = self.methods.grep /^fact_read_/ + fact_readers.each do |getter| + getter =~ /^fact_read_(\w+)/ + fact = $1.to_sym + self.send( getter, @ndev, @known ) unless @known[fact] + end + end + + private + + def examine_model(facts) + model = facts[:hardwaremodel] + if model == 'Virtual Chassis' + re = facts.detect {|k,v| k.match(/^RE\d+/)} + if re + re_model = re[1][:model] + if re_model + model = re_model.start_with?('RE-') ? re_model[3, re_model.length] : re_model + end + end + end + model + end + end # class +end + +### ----------------------------------------------------------------- +### Load all of the fact files +### ----------------------------------------------------------------- + +require 'junos-ez/facts/chassis' +require 'junos-ez/facts/personality' +require 'junos-ez/facts/version' +require 'junos-ez/facts/switch_style' +require 'junos-ez/facts/ifd_style' diff --git a/lib/junos-ez/facts/personality.rb b/lib/junos-ez/facts/personality.rb index 4d8c35d..c4401f8 100644 --- a/lib/junos-ez/facts/personality.rb +++ b/lib/junos-ez/facts/personality.rb @@ -1,25 +1,21 @@ -Junos::Ez::Facts::Keeper.define( :personality ) do |ndev, facts| - - uses :chassis, :routingengines - model = facts[:hardwaremodel] - - 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)/i - :SWITCH - when /^MX/i - :MX - when /^vMX/i - facts[:virtual] = true - :MX - when /SRX(\d){3}/i - :SRX_BRANCH - when /junosv-firefly/i - facts[:virtual] = true - :SRX_BRANCH - when /SRX(\d){4}/i - :SRX_HIGHEND - end - -end +Junos::Ez::Facts::Keeper.define( :personality ) do |ndev, facts| + uses :chassis, :routingengines + + model = examine_model(facts) + facts[:personality] = case model + when /^(?:EX)|(?:QFX)|(?:OCX)/i + :SWITCH + when /^MX/i + :MX + when /^vMX/i + facts[:virtual] = true + :MX + when /SRX(?:\d){3}/i + :SRX_BRANCH + when /junosv-firefly/i + facts[:virtual] = true + :SRX_BRANCH + when /SRX(?:\d){4}/i + :SRX_HIGHEND + end +end diff --git a/lib/junos-ez/facts/switch_style.rb b/lib/junos-ez/facts/switch_style.rb index da98f53..34b864a 100644 --- a/lib/junos-ez/facts/switch_style.rb +++ b/lib/junos-ez/facts/switch_style.rb @@ -1,31 +1,26 @@ -Junos::Ez::Facts::Keeper.define( :switch_style ) do |ndev, facts| - f_persona = uses :personality - - model = facts[:hardwaremodel] - examine = ( model != "Virtual Chassis" ) ? model : facts.select {|k,v| k.match(/^RE[0-9]+/) }.values[0][:model] - - facts[:switch_style] = case f_persona - when :SWITCH, :SRX_BRANCH - case examine - when /junosv-firefly/i - :NONE - when /^(ex9)|(ex43)|(ocx)/i - :VLAN_L2NG - when /^(qfx)/i - if facts[:version][0..3].to_f >= 13.2 - :VLAN_L2NG - else - :VLAN - end - else - :VLAN - end - when :MX, :SRX_HIGHEND - :BRIDGE_DOMAIN - else - :NONE - end - -end - - +Junos::Ez::Facts::Keeper.define( :switch_style ) do |ndev, facts| + f_persona = uses :personality + model = examine_model(facts) + + facts[:switch_style] = case f_persona + when :SWITCH, :SRX_BRANCH + case model + when /junosv-firefly/i + :NONE + when /^(?:ex9)|(?:ex43)|(?:ex34)|(?:ocx)/i + :VLAN_L2NG + when /^qfx/i + if facts[:version][0..3].to_f >= 13.2 + :VLAN_L2NG + else + :VLAN + end + else + :VLAN + end + when :MX, :SRX_HIGHEND + :BRIDGE_DOMAIN + else + :NONE + end +end diff --git a/lib/junos-ez/facts/version.rb b/lib/junos-ez/facts/version.rb index d4fc740..4ba3354 100644 --- a/lib/junos-ez/facts/version.rb +++ b/lib/junos-ez/facts/version.rb @@ -1,58 +1,57 @@ -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_' + "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 +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 + + jun_ver = re_sw.at_xpath('//junos-version') + if jun_ver + facts[ver_key] = jun_ver.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_' + "FPC" + master_id).to_sym] + end + else + jun_ver = swver.at_xpath('//junos-version') + if jun_ver + facts[:version] = jun_ver.text + else + junos = swver.xpath('//package-information[name = "junos"]/comment').text + junos =~ /\[(.*)\]/ + facts[:version] = $1 + end + end +end diff --git a/lib/junos-ez/l2_ports/vlan_l2ng.rb b/lib/junos-ez/l2_ports/vlan_l2ng.rb index 749d1d5..95be501 100644 --- a/lib/junos-ez/l2_ports/vlan_l2ng.rb +++ b/lib/junos-ez/l2_ports/vlan_l2ng.rb @@ -1,502 +1,478 @@ -class Junos::Ez::L2ports::Provider::VLAN_L2NG < 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 the ethernet-switching 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/ethernet-switching 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/ethernet-switching') - 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('vlan/members')){ |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('vlan/members').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 - - 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 ) - } - 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 - - 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(:'ethernet-switching') { - 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 - ## ethernet-switching stanza - - 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 - - ## ---------------------------------------------------------------- - ## :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 - xml.vlan { - del.each { |v| xml.members v, Netconf::JunosConfig::DELETE } - add.each { |v| xml.members 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::VLAN_L2NG - - # 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 ) - this._xml_rm_ac_untagged_vlan( xml ) - end - - def self.ac_tr_nountg( this, xml ) - unless (untg_vlan = this.has[:untagged_vlan]).nil? - this._xml_rm_ac_untagged_vlan( xml ) - end - end - - def self.tr_ac_nountg( this, xml ) - this._delete_native_vlan_id( xml ) - this._xml_rm_these_vlans( xml, this.has[:tagged_vlans ] ) if this.has[:tagged_vlans] - 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 ) - this._xml_rm_ac_untagged_vlan( xml ) - xml.vlan { - xml.members this.should[:untagged_vlan] - } - end - - def self.ac_tr_untg( this, xml ) - # move untagged vlan to native-vlan-id ... - 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 ) - this._xml_rm_these_vlans( xml, this.has[:tagged_vlans ] ) if this.has[:tagged_vlans] - xml.vlan { xml.members this.should[:untagged_vlan] } - 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::VLAN_L2NG - - def build_list - begin - got = @ndev.rpc.get_ethernet_switching_interface_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::VLAN_L2NG - private - - def _get_eth_port_vlans_h( ifs_name ) - - got = @ndev.rpc.get_ethernet_switching_interface_information(:interface_name => 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('l2iff-interface-vlan-name').text.strip - tgdy = vlan.xpath('l2iff-interface-vlan-member-tagness').text.strip - if tgdy == 'untagged' - ret_h[:untagged] = vlan_name - else - ret_h[:tagged] << vlan_name - end - end - ret_h - end - -end - - -### --------------------------------------------------------------- -### [edit vlans] - for interfaces configured here ... -### --------------------------------------------------------------- - -class Junos::Ez::L2ports::Provider::VLAN_L2NG - - def _xml_edit_under_vlans( xml ) - Nokogiri::XML::Builder.with( xml.doc.root ) do |dot| - dot.vlans { - return dot - } - end - end - - def _xml_rm_under_vlans( xml, vlans ) - if vlans.any? - at_vlans = _xml_edit_under_vlans( xml ) - vlans.each do |vlan_name| - Nokogiri::XML::Builder.with( at_vlans.parent ) do |this| - this.vlan { - this.name vlan_name - this.interface( Netconf::JunosConfig::DELETE ) { this.name @name } - } - end - end - end - end - - def _xml_rm_ac_untagged_vlan( xml ) - if @under_vlans.empty? - xml.vlan Netconf::JunosConfig::DELETE - else - _xml_rm_under_vlans( xml, [ @has[:untagged_vlan ] ] ) - @under_vlans = [] - end - end - - def _xml_rm_these_vlans( xml, vlans ) - if @under_vlans.empty? - xml.vlan( Netconf::JunosConfig::DELETE ) - else - # could be a mix between [edit vlans] and [edit interfaces] ... - v_has = vlans.to_set - del_under_vlans = v_has & @under_vlans - _xml_rm_under_vlans( xml, del_under_vlans ) - if v_has ^ @under_vlans - xml.vlan( Netconf::JunosConfig::DELETE ) - end - @under_vlans = [] - end - end - -end - - -class Junos::Ez::L2ports::Provider::VLAN_L2NG - - def _vlan_name_to_tag_id( vlan_name ) - tag_id = @ndev.rpc.get_configuration { |xml| - xml.vlans { xml.vlan { xml.name vlan_name }} - }.xpath('//vlan-id').text.chomp - - raise ArgumentError, "VLAN '#{vlan_name}' not found" if tag_id.empty? - return tag_id - end - - def _vlan_tag_id_to_name( tag_id, my_hash ) - # get the candidate configuration for each VLAN named in tagged_vlans and - # then map it to the corresponding vlan-id. this is not very effecient, but - # at present there is no other way without getting into a cache mech. - vlan_name = @ndev.rpc.get_configuration { |xml| - xml.vlans { - my_hash[:tagged_vlans].each do |v_name| - xml.vlan { - xml.name v_name - xml.send(:'vlan-id') - } - end - } - }.xpath("//vlan[vlan-id = '#{tag_id}']/name").text.chomp - - raise ArgumentError, "VLAN-ID '#{tag_id}' not found" if vlan_name.empty? - return vlan_name - end - -end - -class Junos::Ez::L2ports::Provider::VLAN_L2NG - def _at_native_vlan_id( xml ) - - ifd - end - - def _delete_native_vlan_id( xml ) - Nokogiri::XML::Builder.with( @ifd ) do |dot| - dot.send :'native-vlan-id', Netconf::JunosConfig::DELETE - end - return true - end - - def _set_native_vlan_id( xml, vlan_name ) - Nokogiri::XML::Builder.with( @ifd ) do |dot| - dot.send :'native-vlan-id', _vlan_name_to_tag_id( vlan_name ) - end - return true - end -end +class Junos::Ez::L2ports::Provider::VLAN_L2NG < 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 the ethernet-switching 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/ethernet-switching 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/ethernet-switching') + 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('vlan/members')){ |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] + tagged_vlans_a = f_eth.xpath('vlan/members').collect {|v| str_int_range_to_a(v.text.chomp)} + tagged_vlans_a.flatten! + as_hash[:tagged_vlans] = tagged_vlans_a.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 + + 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 ) + } + 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 + + 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(:'ethernet-switching') { + 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 + ## ethernet-switching stanza + + 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 + + ## ---------------------------------------------------------------- + ## :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 + xml.vlan { + del.each { |v| xml.members v, Netconf::JunosConfig::DELETE } + add.each { |v| xml.members 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::VLAN_L2NG + # 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 ) + this._xml_rm_ac_untagged_vlan( xml ) + end + + def self.ac_tr_nountg( this, xml ) + unless (untg_vlan = this.has[:untagged_vlan]).nil? + this._xml_rm_ac_untagged_vlan( xml ) + end + end + + def self.tr_ac_nountg( this, xml ) + this._delete_native_vlan_id( xml ) + this._xml_rm_these_vlans( xml, this.has[:tagged_vlans ] ) if this.has[:tagged_vlans] + 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 ) + this._xml_rm_ac_untagged_vlan( xml ) + xml.vlan { + xml.members this.should[:untagged_vlan] + } + end + + def self.ac_tr_untg( this, xml ) + # move untagged vlan to native-vlan-id ... + 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 ) + this._xml_rm_these_vlans( xml, this.has[:tagged_vlans ] ) if this.has[:tagged_vlans] + xml.vlan { xml.members this.should[:untagged_vlan] } + 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::VLAN_L2NG + def build_list + begin + got = @ndev.rpc.get_ethernet_switching_interface_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::VLAN_L2NG + private + + def _get_eth_port_vlans_h( ifs_name ) + got = @ndev.rpc.get_ethernet_switching_interface_information(:interface_name => 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('l2iff-interface-vlan-name').text.strip + tgdy = vlan.xpath('l2iff-interface-vlan-member-tagness').text.strip + if tgdy == 'untagged' + ret_h[:untagged] = vlan_name + else + ret_h[:tagged] << vlan_name + end + end + ret_h + end +end + + +### --------------------------------------------------------------- +### [edit vlans] - for interfaces configured here ... +### --------------------------------------------------------------- + +class Junos::Ez::L2ports::Provider::VLAN_L2NG + def _xml_edit_under_vlans( xml ) + Nokogiri::XML::Builder.with( xml.doc.root ) do |dot| + dot.vlans { + return dot + } + end + end + + def _xml_rm_under_vlans( xml, vlans ) + if vlans.any? + at_vlans = _xml_edit_under_vlans( xml ) + vlans.each do |vlan_name| + Nokogiri::XML::Builder.with( at_vlans.parent ) do |this| + this.vlan { + this.name vlan_name + this.interface( Netconf::JunosConfig::DELETE ) { this.name @name } + } + end + end + end + end + + def _xml_rm_ac_untagged_vlan( xml ) + if @under_vlans.empty? + xml.vlan Netconf::JunosConfig::DELETE + else + _xml_rm_under_vlans( xml, [ @has[:untagged_vlan ] ] ) + @under_vlans = [] + end + end + + def _xml_rm_these_vlans( xml, vlans ) + if @under_vlans.empty? + xml.vlan( Netconf::JunosConfig::DELETE ) + else + # could be a mix between [edit vlans] and [edit interfaces] ... + v_has = vlans.to_set + del_under_vlans = v_has & @under_vlans + _xml_rm_under_vlans( xml, del_under_vlans ) + if v_has ^ @under_vlans + xml.vlan( Netconf::JunosConfig::DELETE ) + end + @under_vlans = [] + end + end +end + +class Junos::Ez::L2ports::Provider::VLAN_L2NG + def _vlan_name_to_tag_id( vlan_name ) + tag_id = @ndev.rpc.get_configuration { |xml| + xml.vlans { xml.vlan { xml.name vlan_name }} + }.xpath('//vlan-id').text.chomp + + raise ArgumentError, "VLAN '#{vlan_name}' not found" if tag_id.empty? + return tag_id + end + + def _vlan_tag_id_to_name( tag_id, my_hash ) + vlan_name = if my_hash[:tagged_vlans].include?(tag_id) + @ndev.rpc.get_configuration.xpath("//vlans/vlan[vlan-id = '#{tag_id}']/name").text.chomp + else + '' + end + raise ArgumentError, "VLAN-ID '#{tag_id}' not found" if vlan_name.empty? + vlan_name + end +end + +class Junos::Ez::L2ports::Provider::VLAN_L2NG + def _at_native_vlan_id( xml ) + ifd + end + + def _delete_native_vlan_id( xml ) + Nokogiri::XML::Builder.with( @ifd ) do |dot| + dot.send :'native-vlan-id', Netconf::JunosConfig::DELETE + end + return true + end + + def _set_native_vlan_id( xml, vlan_name ) + Nokogiri::XML::Builder.with( @ifd ) do |dot| + dot.send :'native-vlan-id', _vlan_name_to_tag_id( vlan_name ) + end + return true + end +end diff --git a/lib/junos-ez/provider.rb b/lib/junos-ez/provider.rb index 7a11b79..7c7c39c 100644 --- a/lib/junos-ez/provider.rb +++ b/lib/junos-ez/provider.rb @@ -1,619 +1,616 @@ -##### --------------------------------------------------------------- -##### The Junos::Ez::Provider and associated Parent class make up -##### the main 'framework' of the "EZ" library system. Please -##### consider your changes carefully as it will have a large -##### scope of impact. Thank you. -##### --------------------------------------------------------------- - -require 'set' -require 'junos-ez/version.rb' -require 'junos-ez/exceptions.rb' - -module Junos::Ez - - ### --------------------------------------------------------------- - ### rpc_errors - decodes the XML into an array of error/Hash - ### @@@ TBD: this should be moved into the 'netconf' gem - ### --------------------------------------------------------------- - - def self.rpc_errors( as_xml ) - errs = as_xml.xpath('//rpc-error') - return nil if errs.count == 0 # safety check - - retval = [] - errs.each do |err| - err_h = {} - # every error has a severity and message - err_h[:severity] = err.xpath('error-severity').text.strip - err_h[:message] = err.xpath('error-message').text.strip - - # some have an edit path error - unless ( err_path = err.xpath('error-path')).empty? - err_h[:edit_path] = err_path.text.strip - end - - # some have addition error-info/bad-element ... - unless ( bad_i = err.xpath('error-info/bad-element')).empty? - err_h[:bad_identifier] = bad_i.text.strip - end - - retval << err_h - end - retval - end - -end - -module Junos::Ez::Provider - - ## all managed objects have the following properties: - - PROPERTIES = [ - :_exist, # exists in configuration (or should) - :_active # active in configuration (or should) - ] - - ## 'attach_instance_variable' is the way to dynamically - ## add an instance variable to the on_obj and "publish" - ## it in the same way attr_accessor would. - - def self.attach_instance_variable( on_obj, varsname, new_obj ) - ivar = ("@" + varsname.to_s).to_sym - on_obj.instance_variable_set( ivar, new_obj ) - on_obj.define_singleton_method( varsname ) do - on_obj.instance_variable_get( ivar ) - end - on_obj.providers << varsname - end - -end - -class Junos::Ez::Provider::Parent - - attr_reader :ndev, :parent, :name - attr_accessor :providers - attr_accessor :has, :should, :properties - attr_accessor :list, :catalog - - # p_obj - the parent object - # name - the name of the resource, or nil if this is a provider - # opts - options to the provider/resource. :parent is reserved - - def initialize( p_obj, name = nil, opts = {} ) - - @providers = [] - @parent = opts[:parent] || nil - @ndev = p_obj.instance_variable_get(:@ndev) || p_obj - @name = name - @opts = opts - - @list = [] # array list of item names - @catalog = {} # hash catalog of named items - - return unless @name - # resources only from here ... - @has = {} # properties read-from Junos - @should = {} # properties to write-back to Junos - end - - ### --------------------------------------------------------------- - ### 'is_provider?' - indicates if this object instance is a - ### provider object, rather than a specific instance of the object - ### --------------------------------------------------------------- - - def is_provider?; @name.nil? end - - ### --------------------------------------------------------------- - ### is_new? - indicates if this is a new resource - ### --------------------------------------------------------------- - - def is_new?; (@has[:_exist] == false) || false end - - ### --------------------------------------------------------------- - ### [property] resource property reader or - ### ["name"] resource selector from provider - ### --------------------------------------------------------------- - - def []( property ) - return self.select( property ) if is_provider? - - # if there is already something in the write-back, then use - # it before using from the read-cache - - return @should[property] if @should[property] - return @has[property] if @has - end - - ### --------------------------------------------------------------- - ### []= property writer (@should) - ### --------------------------------------------------------------- - - def []=( property, rval ) - raise ArgumentError, "This is not a provider instance" if is_provider? - raise ArgumentError, "Invalid property['#{property.to_s}']" unless properties.include? property - - @should[property] = rval - end - - ### --------------------------------------------------------------- - ### 'select' a resource from a provider - ### --------------------------------------------------------------- - - def select( name ) - raise ArgumentError, "This is not a provider instance" unless is_provider? - this = self.class.new( @ndev, name, @opts ) - this.properties = self.properties - this.read! - this - end - - ### --------------------------------------------------------------- - ### 'exists?' - does the resource exist in the Juos config - ### --------------------------------------------------------------- - - def exists?; @has[:_exist]; end - - ### --------------------------------------------------------------- - ### 'active?' - is the resource config active in Junos - ### --------------------------------------------------------------- - - def active? - false unless exists? - @has[:_active] - end - - ### @@@ helper method, probably needs to go into 'private section - ### @@@ TBD - - def name_decorated( name = @name ) - self.class.to_s + "['" + name + "']" - end - - ### --------------------------------------------------------------- - ### Provider methods to obtain collection information as - ### 'list' - array of named items - ### 'catalog' - hash of all items with properties - ### --------------------------------------------------------------- - - def list - @list.empty? ? list! : @list - end - - def list! - @list.clear - @list = build_list - end - - def catalog - @catalog.empty? ? catalog! : @catalog - end - - def catalog! - @catalog.clear - @catalog = build_catalog - end - - ### --------------------------------------------------------------- - ### CREATE methods - ### --------------------------------------------------------------- - - ## ---------------------------------------------------------------- - ## 'create' will build a new object, but does not write the - ## contents back to the device. The caller can chain the - ## write! method if desired Alternative, the caller - ## can use 'create!' which does write to the device. - ## ---------------------------------------------------------------- - - def create( name = nil, prop_hash = {} ) - - ## if this is an existing object, then we shouldn't - ## allow the caller to create something. - - raise ArgumentError, "Not called by provider!" unless is_provider? - - ## if we're here, then we're creating an entirely new - ## instance of this object. We should check to see if - ## it first exists, eh? So allow the caller to specify - ## if they want an exception if it already exists; overloading - ## the use of the prop_hash[:_exist], yo! - - newbie = self.select( name ) - if prop_hash[:_exist] - raise ArgumentError, name_decorated(name) + " already exists" if newbie.exists? - end - - prop_hash.each{ |k,v| newbie[k] = v } unless prop_hash.empty? - - ## default mark the newly created object as should exist and should - ## be active (if not already set) - - newbie[:_exist] = true - newbie[:_active] ||= true - - ## if a block is provided, then pass the block the new object - ## the caller is then expected to set the properies - - yield( newbie ) if block_given? - - ## return the new object - return newbie - end - - ## ---------------------------------------------------------------- - ## 'create!' is just a helper to call create and then write - ## the config assuming create returns ok. - ## ---------------------------------------------------------------- - - def create!( *args ) - newbie = create( *args ) - return nil unless newbie - newbie.write! - newbie - end - - ## ---------------------------------------------------------------- - ## YAML / HASH methods - ## ---------------------------------------------------------------- - - def create_from_yaml!( opts = {} ) - raise ArgumentError "Missing :filename param" unless opts[:filename] - as_hash = YAML.load_file( opts[:filename] ) - write_xml_config! xml_from_h_expanded( as_hash, opts ) - end - - def create_from_hash!( as_hash, opts = {} ) - write_xml_config! xml_from_h_expanded( as_hash, opts ) - end - - def to_h_expanded( opts = {} ) - to_h( opts ) - end - - def to_yaml( opts = {} ) - out_hash = to_h_expanded( opts ) - out_yaml = out_hash.to_yaml - File.open( opts[:filename], "w" ){|f| f.puts out_hash.to_yaml } if opts[:filename] - out_yaml - end - - ### --------------------------------------------------------------- - ### 'delete!' will cause the item to be removed from the Junos - ### configuration - ### --------------------------------------------------------------- - - def delete! - return nil unless exists? - xml = xml_at_top - par = xml.instance_variable_get(:@parent) - par['delete'] = 'delete' - xml_on_delete( xml ) - rsp = write_xml_config!( xml.doc.root ) - @has[:_exist] = false - true # rsp ... don't return XML, but let's hear from the community... - end - - ### --------------------------------------------------------------- - ### Junos activation controls - ### --------------------------------------------------------------- - - def activate! - return nil if @should[:_active] == true - @should[:_active] = true - write! - end - - def deactivate! - return nil if @should[:_active] == false - @should[:_active] = false - write! - end - - ### --------------------------------------------------------------- - ### Junos rename element - ### --------------------------------------------------------------- - - ## by default, simply allow the new name - def xml_element_newname( new_name); new_name end - - def rename!( new_name ) - return nil unless exists? - - xml = xml_at_top - par = xml.instance_variable_get(:@parent) - new_ele_name = xml_element_newname( new_name ) - - return nil unless new_ele_name - - par['rename'] = 'rename' - par['name'] = new_ele_name - - rsp = write_xml_config!( xml.doc.root ) - @name = new_name - rsp - end - - ### --------------------------------------------------------------- - ### Junos reorder method - ### - ### opts[:before] = item-name, - ### opts[:after] = item-name - ### --------------------------------------------------------------- - - def reorder!( opts ) - return nil unless exists? - - ## validate opts hash - ctrl, name = opts.first - raise ArgumentError, "Invalid operation #{ctrl}" unless [:before,:after].include? ctrl - - xml = xml_at_top - par = xml.instance_variable_get(:@parent) - par['insert'] = ctrl.to_s - par['name'] = name - rsp = write_xml_config! ( xml.doc.root ) - - return rsp - end - - ### --------------------------------------------------------------- - ### Provider `each` - iterate through each managed resource - ### as obtained from the `list` instance variable. select the - ### object and pass it to the provided block - ### --------------------------------------------------------------- - - def each( &block ) - raise ArgumentError, "not a provider" unless is_provider? - list.each{ |name| yield select(name ) } - end - - ### --------------------------------------------------------------- - ### Provider `with` - iterate through each managed resource - ### as obtained from the `given_list` instance variable. - ### select the object and pass it to the provided block - ### --------------------------------------------------------------- - - def with( given_list, &block ) - raise ArgumentError, "not a provider" unless is_provider? - given_list.each{ |name| yield select( name ) } - end - - ### --------------------------------------------------------------- - ### Provider reader methods - ### --------------------------------------------------------------- - - ## 'init_has' is called when creating a new managed object - ## or when a caller attempts to retrieve a non-existing one - - def init_has; nil end - - ## 'xml_get_has_xml' - used to retrieve the starting location of the - ## actual XML data for the managed object (as compared to the top - ## of the configuration document - - def xml_get_has_xml( xml ); nil end - - ## 'xml_config_read!' is ued to retrieve the configuration - ## from the Junos device - - def xml_config_read! - @ndev.rpc.get_configuration( xml_at_top ) - end - - def read! - @has.clear - cfg_xml = xml_config_read! - @has_xml = xml_get_has_xml( cfg_xml ) - - ## if the thing doesn't exist in Junos, then mark the @has - ## structure accordingly and call the object init_has for - ## any defaults - - unless @has_xml - @has[:_exist] ||= false - @has[:_active] ||= true - init_has - return nil - end - - ## xml_read_parser *MUST* be implmented by the provider class - ## it is used to parse the XML into the HASH structure. It - ## returns true/false - - xml_read_parser( @has_xml, @has ) - - ## return the Hash representation - self.has - end - - ### --------------------------------------------------------------- - ### Provider writer methods - ### --------------------------------------------------------------- - - def need_write?; not @should.empty? end - - def write! - return nil if @should.empty? - - @should[:_exist] ||= true - - # create the necessary chagnes and push them to the Junos - # device. If an error occurs, it will be raised - - xml_change = xml_build_change - return nil unless xml_change - rsp = write_xml_config!( xml_change ) - - # copy the 'should' values into the 'has' values now that - # they've been written back to Junos - - @has.merge! @should - @should.clear - - # returning 'true' for now. might need to change this back - # to 'rsp' depending on the community feedback. general approach is to not have to - # deal with XML, unless it's an exception case. the only time rsp is really - # needed is to look at warnings; i.e. not-errors. errors will generate an exception, yo! - - return true - end - - ### --------------------------------------------------------------- - ### XML writer methods - ### --------------------------------------------------------------- - - def xml_at_edit; nil; end - def xml_at_top; nil; end - def xml_on_create( xml ); nil; end - def xml_on_delete( xml ); nil; end - - def xml_change__exist( xml ) - return xml_on_create( xml ) if @should[:_exist] - - par = xml.instance_variable_get(:@parent) - par['delete'] = 'delete' - - return xml_on_delete( xml ) - end - - ## 'xml_build_change' is used to create the Junos XML - ## configuration structure. Generally speaking it - ## should not be called by code outside the providers, - ## but sometimes we might want to, so don't make it private - - def xml_build_change( xml_at_here = nil ) - edit_at = xml_at_here || xml_at_edit || xml_at_top - - if @should[:_exist] == false - xml_change__exist( edit_at ) - return edit_at.doc.root - end - - changed = false - @should.keys.each do |prop| - changed = true if self.send( "xml_change_#{prop}", edit_at ) - end - (changed) ? edit_at.doc.root : nil - end - - ### --------------------------------------------------------------- - ### XML common write "change" methods - ### --------------------------------------------------------------- - - def xml_change_admin( xml ) - xml.disable (@should[:admin] == :up ) ? Netconf::JunosConfig::DELETE : nil - end - - def xml_change_description( xml ) - xml_set_or_delete( xml, 'description', @should[:description] ) - end - - def xml_change__active( xml ) - par = xml.instance_variable_get(:@parent) - value = @should[:_active] ? 'active' : 'inactive' - par[value] = value # attribute name is same as value - end - - ### --------------------------------------------------------------- - ### 'to_h' lets us look at the read/write hash structures - ### --------------------------------------------------------------- - - def to_h( which = :read ) - { @name => (which == :read) ? @has : @should } - end - - ##### !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - ##### !!!!! PRIVATE METHODS !!!!! - ##### !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - - private - - def set_has_status( xml, has ) - has[:_active] = xml['inactive'] ? false : true - has[:_exist] = true - end - - ### --------------------------------------------------------------- - ### write configuration to Junos. Check for errors vs. warnings. - ### if there are warnings then return the result. If there are - ### errors, re-throw the exception object. If everything was - ### OK, simply return the result - ### --------------------------------------------------------------- - - def write_xml_config!( xml, opts = {} ) - begin - action = {'action' => 'replace' } - result = @ndev.rpc.load_configuration( xml, action ) - rescue Netconf::RpcError => e - errs = e.rsp.xpath('//rpc-error') - raise e unless errs.empty? - e.rsp - else - result - end - end - - def oh_no! - return if @opts[:ignore_raise] - yield if block_given? # should always be a block given ... - end - - ### --------------------------------------------------------------- - ### XML property reader/writer for elements that can be present, - ### or existing with a "no-" prepended. For example "retain" or - ### "no-retain" - ### --------------------------------------------------------------- - - def xml_read_parse_noele( as_xml, ele_name, as_hash, prop ) - unless (ele = as_xml.xpath("#{ele_name} | no-#{ele_name}")).empty? - as_hash[prop] = (ele[0].name =~ /^no-/) ? false : true - end - end - - def xml_set_or_delete_noele( xml, ele_name, prop = ele_name.to_sym ) - - # delete what was there - unless @has[prop].nil? - value_prop = @has[prop] - wr_ele_name = value_prop ? ele_name : 'no-' + ele_name - xml.send(wr_ele_name.to_sym, Netconf::JunosConfig::DELETE) - end - - # if we're not adding anything back, signal that we've done - # something, and we're done, yo! - return true if @should[prop].nil? - - # add new value - value_prop = @should[prop] - ele_name = 'no-' + ele_name if value_prop == false - xml.send( ele_name.to_sym ) - - end - - def xml_when_item( xml_item, &block ) - raise ArgumentError, "no block given" unless block_given? - return unless xml_item[0] - return yield(xml_item[0]) if block.arity == 1 - yield - end - - ### --------------------------------------------------------------- - ### XML property writer utilities - ### --------------------------------------------------------------- - - def xml_set_or_delete( xml, ele_name, value ) - xml.send( ele_name.to_sym, (value ? value : Netconf::JunosConfig::DELETE) ) - end - - def xml_set_or_delete_element( xml, ele_name, should ) - xml.send( ele_name.to_sym, (should) ? nil : Netconf::JunosConfig::DELETE ) - end - - def diff_property_array( prop ) - should = @should[prop] || [] - has = @has[prop] || [] - [ should - has, has - should ] - end - -end - - +##### --------------------------------------------------------------- +##### The Junos::Ez::Provider and associated Parent class make up +##### the main 'framework' of the "EZ" library system. Please +##### consider your changes carefully as it will have a large +##### scope of impact. Thank you. +##### --------------------------------------------------------------- + +require 'set' +require 'junos-ez/version.rb' +require 'junos-ez/exceptions.rb' + +module Junos::Ez + ### --------------------------------------------------------------- + ### rpc_errors - decodes the XML into an array of error/Hash + ### @@@ TBD: this should be moved into the 'netconf' gem + ### --------------------------------------------------------------- + + def self.rpc_errors( as_xml ) + errs = as_xml.xpath('//rpc-error') + return nil if errs.count == 0 # safety check + + retval = [] + errs.each do |err| + err_h = {} + # every error has a severity and message + err_h[:severity] = err.xpath('error-severity').text.strip + err_h[:message] = err.xpath('error-message').text.strip + + # some have an edit path error + unless ( err_path = err.xpath('error-path')).empty? + err_h[:edit_path] = err_path.text.strip + end + + # some have addition error-info/bad-element ... + unless ( bad_i = err.xpath('error-info/bad-element')).empty? + err_h[:bad_identifier] = bad_i.text.strip + end + + retval << err_h + end + retval + end +end + +module Junos::Ez::Provider + ## all managed objects have the following properties: + + PROPERTIES = [ + :_exist, # exists in configuration (or should) + :_active # active in configuration (or should) + ] + + ## 'attach_instance_variable' is the way to dynamically + ## add an instance variable to the on_obj and "publish" + ## it in the same way attr_accessor would. + + def self.attach_instance_variable( on_obj, varsname, new_obj ) + ivar = ("@" + varsname.to_s).to_sym + on_obj.instance_variable_set( ivar, new_obj ) + on_obj.define_singleton_method( varsname ) do + on_obj.instance_variable_get( ivar ) + end + on_obj.providers << varsname + end +end + +class Junos::Ez::Provider::Parent + attr_reader :ndev, :parent, :name + attr_accessor :providers + attr_accessor :has, :should, :properties + attr_accessor :list, :catalog + + # p_obj - the parent object + # name - the name of the resource, or nil if this is a provider + # opts - options to the provider/resource. :parent is reserved + + def initialize( p_obj, name = nil, opts = {} ) + @providers = [] + @parent = opts[:parent] || nil + @ndev = p_obj.instance_variable_get(:@ndev) || p_obj + @name = name + @opts = opts + + @list = [] # array list of item names + @catalog = {} # hash catalog of named items + + return unless @name + # resources only from here ... + @has = {} # properties read-from Junos + @should = {} # properties to write-back to Junos + end + + ### --------------------------------------------------------------- + ### 'is_provider?' - indicates if this object instance is a + ### provider object, rather than a specific instance of the object + ### --------------------------------------------------------------- + + def is_provider?; @name.nil? end + + ### --------------------------------------------------------------- + ### is_new? - indicates if this is a new resource + ### --------------------------------------------------------------- + + def is_new?; (@has[:_exist] == false) || false end + + ### --------------------------------------------------------------- + ### [property] resource property reader or + ### ["name"] resource selector from provider + ### --------------------------------------------------------------- + + def []( property ) + return self.select( property ) if is_provider? + + # if there is already something in the write-back, then use + # it before using from the read-cache + + return @should[property] if @should[property] + return @has[property] if @has + end + + ### --------------------------------------------------------------- + ### []= property writer (@should) + ### --------------------------------------------------------------- + + def []=( property, rval ) + raise ArgumentError, "This is not a provider instance" if is_provider? + raise ArgumentError, "Invalid property['#{property.to_s}']" unless properties.include? property + + @should[property] = rval + end + + ### --------------------------------------------------------------- + ### 'select' a resource from a provider + ### --------------------------------------------------------------- + + def select( name ) + raise ArgumentError, "This is not a provider instance" unless is_provider? + this = self.class.new( @ndev, name, @opts ) + this.properties = self.properties + this.read! + this + end + + ### --------------------------------------------------------------- + ### 'exists?' - does the resource exist in the Juos config + ### --------------------------------------------------------------- + + def exists?; @has[:_exist]; end + + ### --------------------------------------------------------------- + ### 'active?' - is the resource config active in Junos + ### --------------------------------------------------------------- + + def active? + false unless exists? + @has[:_active] + end + + ### @@@ helper method, probably needs to go into 'private section + ### @@@ TBD + + def name_decorated( name = @name ) + self.class.to_s + "['" + name + "']" + end + + ### --------------------------------------------------------------- + ### Provider methods to obtain collection information as + ### 'list' - array of named items + ### 'catalog' - hash of all items with properties + ### --------------------------------------------------------------- + + def list + @list.empty? ? list! : @list + end + + def list! + @list.clear + @list = build_list + end + + def catalog + @catalog.empty? ? catalog! : @catalog + end + + def catalog! + @catalog.clear + @catalog = build_catalog + end + + ### --------------------------------------------------------------- + ### CREATE methods + ### --------------------------------------------------------------- + + ## ---------------------------------------------------------------- + ## 'create' will build a new object, but does not write the + ## contents back to the device. The caller can chain the + ## write! method if desired Alternative, the caller + ## can use 'create!' which does write to the device. + ## ---------------------------------------------------------------- + + def create( name = nil, prop_hash = {} ) + ## if this is an existing object, then we shouldn't + ## allow the caller to create something. + + raise ArgumentError, "Not called by provider!" unless is_provider? + + ## if we're here, then we're creating an entirely new + ## instance of this object. We should check to see if + ## it first exists, eh? So allow the caller to specify + ## if they want an exception if it already exists; overloading + ## the use of the prop_hash[:_exist], yo! + + newbie = self.select( name ) + if prop_hash[:_exist] + raise ArgumentError, name_decorated(name) + " already exists" if newbie.exists? + end + + prop_hash.each{ |k,v| newbie[k] = v } unless prop_hash.empty? + + ## default mark the newly created object as should exist and should + ## be active (if not already set) + + newbie[:_exist] = true + newbie[:_active] ||= true + + ## if a block is provided, then pass the block the new object + ## the caller is then expected to set the properies + + yield( newbie ) if block_given? + + ## return the new object + return newbie + end + + ## ---------------------------------------------------------------- + ## 'create!' is just a helper to call create and then write + ## the config assuming create returns ok. + ## ---------------------------------------------------------------- + + def create!( *args ) + newbie = create( *args ) + return nil unless newbie + newbie.write! + newbie + end + + ## ---------------------------------------------------------------- + ## YAML / HASH methods + ## ---------------------------------------------------------------- + + def create_from_yaml!( opts = {} ) + raise ArgumentError "Missing :filename param" unless opts[:filename] + as_hash = YAML.load_file( opts[:filename] ) + write_xml_config! xml_from_h_expanded( as_hash, opts ) + end + + def create_from_hash!( as_hash, opts = {} ) + write_xml_config! xml_from_h_expanded( as_hash, opts ) + end + + def to_h_expanded( opts = {} ) + to_h( opts ) + end + + def to_yaml( opts = {} ) + out_hash = to_h_expanded( opts ) + out_yaml = out_hash.to_yaml + File.open( opts[:filename], "w" ){|f| f.puts out_hash.to_yaml } if opts[:filename] + out_yaml + end + + ### --------------------------------------------------------------- + ### 'delete!' will cause the item to be removed from the Junos + ### configuration + ### --------------------------------------------------------------- + + def delete! + return nil unless exists? + xml = xml_at_top + par = xml.instance_variable_get(:@parent) + par['delete'] = 'delete' + xml_on_delete( xml ) + rsp = write_xml_config!( xml.doc.root ) + @has[:_exist] = false + true # rsp ... don't return XML, but let's hear from the community... + end + + ### --------------------------------------------------------------- + ### Junos activation controls + ### --------------------------------------------------------------- + + def activate! + return nil if @should[:_active] == true + @should[:_active] = true + write! + end + + def deactivate! + return nil if @should[:_active] == false + @should[:_active] = false + write! + end + + ### --------------------------------------------------------------- + ### Junos rename element + ### --------------------------------------------------------------- + + ## by default, simply allow the new name + def xml_element_newname( new_name); new_name end + + def rename!( new_name ) + return nil unless exists? + + xml = xml_at_top + par = xml.instance_variable_get(:@parent) + new_ele_name = xml_element_newname( new_name ) + + return nil unless new_ele_name + + par['rename'] = 'rename' + par['name'] = new_ele_name + + rsp = write_xml_config!( xml.doc.root ) + @name = new_name + rsp + end + + ### --------------------------------------------------------------- + ### Junos reorder method + ### + ### opts[:before] = item-name, + ### opts[:after] = item-name + ### --------------------------------------------------------------- + + def reorder!( opts ) + return nil unless exists? + + ## validate opts hash + ctrl, name = opts.first + raise ArgumentError, "Invalid operation #{ctrl}" unless [:before,:after].include? ctrl + + xml = xml_at_top + par = xml.instance_variable_get(:@parent) + par['insert'] = ctrl.to_s + par['name'] = name + rsp = write_xml_config! ( xml.doc.root ) + + return rsp + end + + ### --------------------------------------------------------------- + ### Provider `each` - iterate through each managed resource + ### as obtained from the `list` instance variable. select the + ### object and pass it to the provided block + ### --------------------------------------------------------------- + + def each( &block ) + raise ArgumentError, "not a provider" unless is_provider? + list.each{ |name| yield select(name ) } + end + + ### --------------------------------------------------------------- + ### Provider `with` - iterate through each managed resource + ### as obtained from the `given_list` instance variable. + ### select the object and pass it to the provided block + ### --------------------------------------------------------------- + + def with( given_list, &block ) + raise ArgumentError, "not a provider" unless is_provider? + given_list.each{ |name| yield select( name ) } + end + + ### --------------------------------------------------------------- + ### Provider reader methods + ### --------------------------------------------------------------- + + ## 'init_has' is called when creating a new managed object + ## or when a caller attempts to retrieve a non-existing one + + def init_has; nil end + + ## 'xml_get_has_xml' - used to retrieve the starting location of the + ## actual XML data for the managed object (as compared to the top + ## of the configuration document + + def xml_get_has_xml( xml ); nil end + + ## 'xml_config_read!' is ued to retrieve the configuration + ## from the Junos device + + def xml_config_read! + @ndev.rpc.get_configuration( xml_at_top ) + end + + def read! + @has.clear + cfg_xml = xml_config_read! + @has_xml = xml_get_has_xml( cfg_xml ) + + ## if the thing doesn't exist in Junos, then mark the @has + ## structure accordingly and call the object init_has for + ## any defaults + + unless @has_xml + @has[:_exist] ||= false + @has[:_active] ||= true + init_has + return nil + end + + ## xml_read_parser *MUST* be implmented by the provider class + ## it is used to parse the XML into the HASH structure. It + ## returns true/false + + xml_read_parser( @has_xml, @has ) + + ## return the Hash representation + self.has + end + + ### --------------------------------------------------------------- + ### Provider writer methods + ### --------------------------------------------------------------- + + def need_write?; not @should.empty? end + + def write! + return nil if @should.empty? + + @should[:_exist] ||= true + + # create the necessary chagnes and push them to the Junos + # device. If an error occurs, it will be raised + + xml_change = xml_build_change + return nil unless xml_change + rsp = write_xml_config!( xml_change ) + + # copy the 'should' values into the 'has' values now that + # they've been written back to Junos + + @has.merge! @should + @should.clear + + # returning 'true' for now. might need to change this back + # to 'rsp' depending on the community feedback. general approach is to not have to + # deal with XML, unless it's an exception case. the only time rsp is really + # needed is to look at warnings; i.e. not-errors. errors will generate an exception, yo! + + return true + end + + ### --------------------------------------------------------------- + ### XML writer methods + ### --------------------------------------------------------------- + + def xml_at_edit; nil; end + def xml_at_top; nil; end + def xml_on_create( xml ); nil; end + def xml_on_delete( xml ); nil; end + + def xml_change__exist( xml ) + return xml_on_create( xml ) if @should[:_exist] + + par = xml.instance_variable_get(:@parent) + par['delete'] = 'delete' + + return xml_on_delete( xml ) + end + + ## 'xml_build_change' is used to create the Junos XML + ## configuration structure. Generally speaking it + ## should not be called by code outside the providers, + ## but sometimes we might want to, so don't make it private + + def xml_build_change( xml_at_here = nil ) + edit_at = xml_at_here || xml_at_edit || xml_at_top + + if @should[:_exist] == false + xml_change__exist( edit_at ) + return edit_at.doc.root + end + + changed = false + @should.keys.each do |prop| + changed = true if self.send( "xml_change_#{prop}", edit_at ) + end + (changed) ? edit_at.doc.root : nil + end + + ### --------------------------------------------------------------- + ### XML common write "change" methods + ### --------------------------------------------------------------- + + def xml_change_admin( xml ) + xml.disable (@should[:admin] == :up ) ? Netconf::JunosConfig::DELETE : nil + end + + def xml_change_description( xml ) + xml_set_or_delete( xml, 'description', @should[:description] ) + end + + def xml_change__active( xml ) + par = xml.instance_variable_get(:@parent) + value = @should[:_active] ? 'active' : 'inactive' + par[value] = value # attribute name is same as value + end + + ### --------------------------------------------------------------- + ### 'to_h' lets us look at the read/write hash structures + ### --------------------------------------------------------------- + + def to_h( which = :read ) + { @name => (which == :read) ? @has : @should } + end + + ##### !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + ##### !!!!! PRIVATE METHODS !!!!! + ##### !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + private + + # decompose a range to idividual values + # str_int_range_to_a('1') # => ["1"] + # str_int_range_to_a('1-4') # => ["1", "2", "3", "4"] + def str_int_range_to_a(str) + return [str] unless str.include?('-') + first, last = str.split('-', 2) + (first.to_i..last.to_i).map(&:to_s) + end + + def set_has_status( xml, has ) + has[:_active] = xml['inactive'] ? false : true + has[:_exist] = true + end + + ### --------------------------------------------------------------- + ### write configuration to Junos. Check for errors vs. warnings. + ### if there are warnings then return the result. If there are + ### errors, re-throw the exception object. If everything was + ### OK, simply return the result + ### --------------------------------------------------------------- + + def write_xml_config!( xml, opts = {} ) + begin + action = {'action' => 'replace' } + result = @ndev.rpc.load_configuration( xml, action ) + rescue Netconf::RpcError => e + errs = e.rsp.xpath('//rpc-error') + raise e unless errs.empty? + e.rsp + else + result + end + end + + def oh_no! + return if @opts[:ignore_raise] + yield if block_given? # should always be a block given ... + end + + ### --------------------------------------------------------------- + ### XML property reader/writer for elements that can be present, + ### or existing with a "no-" prepended. For example "retain" or + ### "no-retain" + ### --------------------------------------------------------------- + + def xml_read_parse_noele( as_xml, ele_name, as_hash, prop ) + unless (ele = as_xml.xpath("#{ele_name} | no-#{ele_name}")).empty? + as_hash[prop] = (ele[0].name =~ /^no-/) ? false : true + end + end + + def xml_set_or_delete_noele( xml, ele_name, prop = ele_name.to_sym ) + # delete what was there + unless @has[prop].nil? + value_prop = @has[prop] + wr_ele_name = value_prop ? ele_name : 'no-' + ele_name + xml.send(wr_ele_name.to_sym, Netconf::JunosConfig::DELETE) + end + + # if we're not adding anything back, signal that we've done + # something, and we're done, yo! + return true if @should[prop].nil? + + # add new value + value_prop = @should[prop] + ele_name = 'no-' + ele_name if value_prop == false + xml.send( ele_name.to_sym ) + end + + def xml_when_item( xml_item, &block ) + raise ArgumentError, "no block given" unless block_given? + return unless xml_item[0] + return yield(xml_item[0]) if block.arity == 1 + yield + end + + ### --------------------------------------------------------------- + ### XML property writer utilities + ### --------------------------------------------------------------- + + def xml_set_or_delete( xml, ele_name, value ) + xml.send( ele_name.to_sym, (value ? value : Netconf::JunosConfig::DELETE) ) + end + + def xml_set_or_delete_element( xml, ele_name, should ) + xml.send( ele_name.to_sym, (should) ? nil : Netconf::JunosConfig::DELETE ) + end + + def diff_property_array( prop ) + should = @should[prop] || [] + has = @has[prop] || [] + [ should - has, has - should ] + end +end