From 92b4b34112d90fbc8e6d24ade7620abe95c29ab8 Mon Sep 17 00:00:00 2001 From: Seth Grover Date: Fri, 27 Oct 2023 10:21:44 -0600 Subject: [PATCH] address issues with NetBox database and Logstash's NetBox cache (idaholab/Malcolm#259); restore caching for performance reasons, but decrease TTL significantly and allow it to be specified via environment variable --- Dockerfiles/logstash.Dockerfile | 4 + config/logstash.env.example | 3 + logstash/pipelines/enrichment/21_netbox.conf | 8 + logstash/ruby/netbox_enrich.rb | 1011 +++++++++--------- 4 files changed, 514 insertions(+), 512 deletions(-) diff --git a/Dockerfiles/logstash.Dockerfile b/Dockerfiles/logstash.Dockerfile index cebe8a6ee..0c2c9258c 100644 --- a/Dockerfiles/logstash.Dockerfile +++ b/Dockerfiles/logstash.Dockerfile @@ -32,6 +32,8 @@ ARG LOGSTASH_NETBOX_ENRICHMENT=false ARG LOGSTASH_NETBOX_ENRICHMENT_VERBOSE=false ARG LOGSTASH_NETBOX_ENRICHMENT_LOOKUP_SERVICE=true ARG LOGSTASH_NETBOX_AUTO_POPULATE=false +ARG LOGSTASH_NETBOX_CACHE_SIZE=1000 +ARG LOGSTASH_NETBOX_CACHE_TTL=30 ENV LOGSTASH_ENRICHMENT_PIPELINE $LOGSTASH_ENRICHMENT_PIPELINE ENV LOGSTASH_PARSE_PIPELINE_ADDRESSES $LOGSTASH_PARSE_PIPELINE_ADDRESSES @@ -42,6 +44,8 @@ ENV LOGSTASH_NETBOX_ENRICHMENT $LOGSTASH_NETBOX_ENRICHMENT ENV LOGSTASH_NETBOX_ENRICHMENT_VERBOSE $LOGSTASH_NETBOX_ENRICHMENT_VERBOSE ENV LOGSTASH_NETBOX_ENRICHMENT_LOOKUP_SERVICE $LOGSTASH_NETBOX_ENRICHMENT_LOOKUP_SERVICE ENV LOGSTASH_NETBOX_AUTO_POPULATE $LOGSTASH_NETBOX_AUTO_POPULATE +ENV LOGSTASH_NETBOX_CACHE_SIZE $LOGSTASH_NETBOX_CACHE_SIZE +ENV LOGSTASH_NETBOX_CACHE_TTL $LOGSTASH_NETBOX_CACHE_TTL USER root diff --git a/config/logstash.env.example b/config/logstash.env.example index 6370a05c1..b5e6f7e56 100644 --- a/config/logstash.env.example +++ b/config/logstash.env.example @@ -13,5 +13,8 @@ LOGSTASH_REVERSE_DNS=false LOGSTASH_NETBOX_ENRICHMENT=false # Whether or not unobserved network entities in Logstash data will be used to populate NetBox LOGSTASH_NETBOX_AUTO_POPULATE=false +# Caching parameters for NetBox's LogStash lookups +LOGSTASH_NETBOX_CACHE_SIZE=1000 +LOGSTASH_NETBOX_CACHE_TTL=30 # Logstash memory allowance and other Java options LS_JAVA_OPTS=-server -Xms2500m -Xmx2500m -Xss1536k -XX:-HeapDumpOnOutOfMemoryError -Djava.security.egd=file:/dev/./urandom -Dlog4j.formatMsgNoLookups=true \ No newline at end of file diff --git a/logstash/pipelines/enrichment/21_netbox.conf b/logstash/pipelines/enrichment/21_netbox.conf index 50731cc12..66c0f34db 100644 --- a/logstash/pipelines/enrichment/21_netbox.conf +++ b/logstash/pipelines/enrichment/21_netbox.conf @@ -31,6 +31,8 @@ filter { "lookup_site_env" => "NETBOX_DEFAULT_SITE" "verbose_env" => "LOGSTASH_NETBOX_ENRICHMENT_VERBOSE" "netbox_token_env" => "SUPERUSER_API_TOKEN" + "cache_size_env" => "LOGSTASH_NETBOX_CACHE_SIZE" + "cache_ttl_env" => "LOGSTASH_NETBOX_CACHE_TTL" } } ruby { @@ -44,6 +46,8 @@ filter { "lookup_service" => "false" "verbose_env" => "LOGSTASH_NETBOX_ENRICHMENT_VERBOSE" "netbox_token_env" => "SUPERUSER_API_TOKEN" + "cache_size_env" => "LOGSTASH_NETBOX_CACHE_SIZE" + "cache_ttl_env" => "LOGSTASH_NETBOX_CACHE_TTL" "autopopulate_env" => "LOGSTASH_NETBOX_AUTO_POPULATE" "default_manuf_env" => "NETBOX_DEFAULT_MANUFACTURER" "default_dtype_env" => "NETBOX_DEFAULT_DEVICE_TYPE" @@ -66,6 +70,8 @@ filter { "lookup_site_env" => "NETBOX_DEFAULT_SITE" "verbose_env" => "LOGSTASH_NETBOX_ENRICHMENT_VERBOSE" "netbox_token_env" => "SUPERUSER_API_TOKEN" + "cache_size_env" => "LOGSTASH_NETBOX_CACHE_SIZE" + "cache_ttl_env" => "LOGSTASH_NETBOX_CACHE_TTL" } } ruby { @@ -80,6 +86,8 @@ filter { "lookup_service_port_source" => "[destination][port]" "verbose_env" => "LOGSTASH_NETBOX_ENRICHMENT_VERBOSE" "netbox_token_env" => "SUPERUSER_API_TOKEN" + "cache_size_env" => "LOGSTASH_NETBOX_CACHE_SIZE" + "cache_ttl_env" => "LOGSTASH_NETBOX_CACHE_TTL" "autopopulate_env" => "LOGSTASH_NETBOX_AUTO_POPULATE" "default_manuf_env" => "NETBOX_DEFAULT_MANUFACTURER" "default_dtype_env" => "NETBOX_DEFAULT_DEVICE_TYPE" diff --git a/logstash/ruby/netbox_enrich.rb b/logstash/ruby/netbox_enrich.rb index 96e4dc31e..fedc370dd 100644 --- a/logstash/ruby/netbox_enrich.rb +++ b/logstash/ruby/netbox_enrich.rb @@ -3,7 +3,6 @@ def concurrency end def register(params) - require 'concurrent' require 'date' require 'faraday' require 'fuzzystringmatch' @@ -46,6 +45,28 @@ def register(params) # API parameters @page_size = params.fetch("page_size", 50) + # caching parameters (default cache size = 1000, default cache TTL = 30 seconds) + _cache_size_val = params["cache_size"] + _cache_size_env = params["cache_size_env"] + if (!_cache_size_val.is_a?(Integer) || _cache_size_val <= 0) && !_cache_size_env.nil? + _cache_size_val = Integer(ENV[_cache_size_env], exception: false) + end + if _cache_size_val.is_a?(Integer) && (_cache_size_val > 0) + @cache_size = _cache_size_val + else + @cache_size = 1000 + end + _cache_ttl_val = params["cache_ttl"] + _cache_ttl_env = params["cache_ttl_env"] + if (!_cache_ttl_val.is_a?(Integer) || _cache_ttl_val <= 0) && !_cache_ttl_env.nil? + _cache_ttl_val = Integer(ENV[_cache_ttl_env], exception: false) + end + if _cache_ttl_val.is_a?(Integer) && (_cache_ttl_val > 0) + @cache_ttl = _cache_ttl_val + else + @cache_ttl = 30 + end + # target field to store looked-up value @target = params["target"] @@ -70,13 +91,6 @@ def register(params) @netbox_url = params.fetch("netbox_url", "http://netbox:8080/netbox/api").delete_suffix("/") @netbox_url_suffix = "/netbox/api" @netbox_url_base = @netbox_url.delete_suffix(@netbox_url_suffix) - @netbox_headers = { 'Content-Type': 'application/json' } - - # netbox connection (will be initialized in a thread-safe manner in filter) - @netbox_conn = nil - @netbox_conn_lock = Concurrent::ReentrantReadWriteLock.new - @netbox_conn_needs_reset = false - @netbox_conn_resetting = false # connection token (either specified directly or read from ENV via netbox_token_env) @netbox_token = params["netbox_token"] @@ -85,6 +99,9 @@ def register(params) @netbox_token = ENV[_netbox_token_env] end + # hash of lookup types (from @lookup_type), each of which contains the respective looked-up values + @cache_hash = LruRedux::ThreadSafeCache.new(params.fetch("lookup_cache_size", 512)) + # these are used for autopopulation only, not lookup/enrichment # autopopulate - either specified directly or read from ENV via autopopulate_env @@ -180,13 +197,13 @@ def register(params) @autopopulate_create_manuf = [1, true, '1', 'true', 't', 'on', 'enabled'].include?(_autopopulate_create_manuf_str.to_s.downcase) # case-insensitive hash of OUIs (https://standards-oui.ieee.org/) to Manufacturers (https://demo.netbox.dev/static/docs/core-functionality/device-types/) - @manuf_hash = LruRedux::ThreadSafeCache.new(params.fetch("manuf_cache_size", 2048)) + @manuf_hash = LruRedux::TTL::ThreadSafeCache.new(params.fetch("manuf_cache_size", 2048), @cache_ttl) # case-insensitive hash of role names to IDs - @role_hash = LruRedux::ThreadSafeCache.new(params.fetch("role_cache_size", 128)) + @role_hash = LruRedux::TTL::ThreadSafeCache.new(params.fetch("role_cache_size", 256), @cache_ttl) # case-insensitive hash of site names to IDs - @site_hash = LruRedux::ThreadSafeCache.new(params.fetch("site_cache_size", 128)) + @site_hash = LruRedux::TTL::ThreadSafeCache.new(params.fetch("site_cache_size", 128), @cache_ttl) # end of autopopulation arguments @@ -202,6 +219,9 @@ def filter(event) _url = @netbox_url _url_base = @netbox_url_base _url_suffix = @netbox_url_suffix + _token = @netbox_token + _cache_size = @cache_size + _cache_ttl = @cache_ttl _page_size = @page_size _verbose = @verbose _lookup_type = @lookup_type @@ -218,527 +238,494 @@ def filter(event) _autopopulate_mac = event.get("#{@source_mac}") _autopopulate_oui = event.get("#{@source_oui}") - _result = nil - _autopopulate_device = nil - _autopopulate_role = nil - _autopopulate_dtype = nil - _autopopulate_interface = nil - _autopopulate_ip = nil - _autopopulate_manuf = nil - _autopopulate_site = nil - _vrfs = nil - _devices = nil - _exception_error_general = false - _exception_error_connection = false - - @netbox_conn_lock.acquire_read_lock - begin - - # make sure the connection to the NetBox API exists and wasn't flagged for reconnect - if @netbox_conn.nil? || @netbox_conn_needs_reset - @netbox_conn_lock.release_read_lock - @netbox_conn_lock.acquire_write_lock - begin - if @netbox_conn.nil? || @netbox_conn_needs_reset - begin - # we need to reconnect to the NetBox API - @netbox_conn_resetting = true - @netbox_conn = Faraday.new(_url) do |conn| - conn.request :authorization, 'Token', @netbox_token - conn.request :url_encoded - conn.response :json, :parser_options => { :symbolize_names => true } - end - ensure - @netbox_conn_resetting = false - @netbox_conn_needs_reset = @netbox_conn.nil? - end - end # connection check in write lock - ensure - @netbox_conn_lock.release_write_lock - @netbox_conn_lock.acquire_read_lock - end - end # connection check in read lock - - # handle :ip_device first, because if we're doing autopopulate we're also going to use - # some of the logic from :ip_vrf - - if (_lookup_type == :ip_device) - ################################################################################# - # retrieve the list of IP addresses where address matches the search key, limited to "assigned" addresses. - # then, for those IP addresses, search for devices pertaining to the interfaces assigned to each - # IP address (e.g., ipam.ip_address -> dcim.interface -> dcim.device, or - # ipam.ip_address -> virtualization.interface -> virtualization.virtual_machine) - _devices = Array.new - _query = { :address => _key, - :offset => 0, - :limit => _page_size } - begin - while true do - if (_ip_addresses_response = @netbox_conn.get('ipam/ip-addresses/', _query).body) && - _ip_addresses_response.is_a?(Hash) - then - _tmp_ip_addresses = _ip_addresses_response.fetch(:results, []) - _tmp_ip_addresses.each do |i| - _is_device = nil - if (_obj = i.fetch(:assigned_object, nil)) && - ((_device_obj = _obj.fetch(:device, nil)) || - (_virtualized_obj = _obj.fetch(:virtual_machine, nil))) - then - _is_device = !_device_obj.nil? - _device = _is_device ? _device_obj : _virtualized_obj - # if we can, follow the :assigned_object's "full" device URL to get more information - _device = (_device.has_key?(:url) && (_full_device = @netbox_conn.get(_device[:url].delete_prefix(_url_base).delete_prefix(_url_suffix).delete_prefix("/")).body)) ? _full_device : _device - _device_id = _device.fetch(:id, nil) - _device_site = ((_site = _device.fetch(:site, nil)) && _site&.has_key?(:name)) ? _site[:name] : _site&.fetch(:display, nil) - next unless (_device_site.to_s.downcase == _lookup_site.to_s.downcase) || _lookup_site.nil? || _lookup_site.empty? || _device_site.nil? || _device_site.empty? - # look up service if requested (based on device/vm found and service port) - if (_lookup_service_port > 0) - _services = Array.new - _service_query = { (_is_device ? :device_id : :virtual_machine_id) => _device_id, :port => _lookup_service_port, :offset => 0, :limit => _page_size } + _result = @cache_hash.getset(_lookup_type){ + LruRedux::TTL::ThreadSafeCache.new(_cache_size, _cache_ttl) + }.getset(_key){ + + _nb = Faraday.new(_url) do |conn| + conn.request :authorization, 'Token', _token + conn.request :url_encoded + conn.response :json, :parser_options => { :symbolize_names => true } + end + _nb_headers = { 'Content-Type': 'application/json' } + + _lookup_result = nil + _autopopulate_device = nil + _autopopulate_role = nil + _autopopulate_dtype = nil + _autopopulate_interface = nil + _autopopulate_ip = nil + _autopopulate_manuf = nil + _autopopulate_site = nil + _vrfs = nil + _devices = nil + _exception_error = false + + # handle :ip_device first, because if we're doing autopopulate we're also going to use + # some of the logic from :ip_vrf + + if (_lookup_type == :ip_device) + ################################################################################# + # retrieve the list of IP addresses where address matches the search key, limited to "assigned" addresses. + # then, for those IP addresses, search for devices pertaining to the interfaces assigned to each + # IP address (e.g., ipam.ip_address -> dcim.interface -> dcim.device, or + # ipam.ip_address -> virtualization.interface -> virtualization.virtual_machine) + _devices = Array.new + _query = { :address => _key, + :offset => 0, + :limit => _page_size } + begin while true do - if (_services_response = @netbox_conn.get('ipam/services/', _service_query).body) && - _services_response.is_a?(Hash) + if (_ip_addresses_response = _nb.get('ipam/ip-addresses/', _query).body) && + _ip_addresses_response.is_a?(Hash) then - _tmp_services = _services_response.fetch(:results, []) - _services.unshift(*_tmp_services) unless _tmp_services.nil? || _tmp_services.empty? - _service_query[:offset] += _tmp_services.length() - break unless (_tmp_services.length() >= _page_size) + _tmp_ip_addresses = _ip_addresses_response.fetch(:results, []) + _tmp_ip_addresses.each do |i| + _is_device = nil + if (_obj = i.fetch(:assigned_object, nil)) && + ((_device_obj = _obj.fetch(:device, nil)) || + (_virtualized_obj = _obj.fetch(:virtual_machine, nil))) + then + _is_device = !_device_obj.nil? + _device = _is_device ? _device_obj : _virtualized_obj + # if we can, follow the :assigned_object's "full" device URL to get more information + _device = (_device.has_key?(:url) && (_full_device = _nb.get(_device[:url].delete_prefix(_url_base).delete_prefix(_url_suffix).delete_prefix("/")).body)) ? _full_device : _device + _device_id = _device.fetch(:id, nil) + _device_site = ((_site = _device.fetch(:site, nil)) && _site&.has_key?(:name)) ? _site[:name] : _site&.fetch(:display, nil) + next unless (_device_site.to_s.downcase == _lookup_site.to_s.downcase) || _lookup_site.nil? || _lookup_site.empty? || _device_site.nil? || _device_site.empty? + # look up service if requested (based on device/vm found and service port) + if (_lookup_service_port > 0) + _services = Array.new + _service_query = { (_is_device ? :device_id : :virtual_machine_id) => _device_id, :port => _lookup_service_port, :offset => 0, :limit => _page_size } + while true do + if (_services_response = _nb.get('ipam/services/', _service_query).body) && + _services_response.is_a?(Hash) + then + _tmp_services = _services_response.fetch(:results, []) + _services.unshift(*_tmp_services) unless _tmp_services.nil? || _tmp_services.empty? + _service_query[:offset] += _tmp_services.length() + break unless (_tmp_services.length() >= _page_size) + else + break + end + end + _device[:service] = _services + end + # non-verbose output is flatter with just names { :name => "name", :id => "id", ... } + # if _verbose, include entire object as :details + _devices << { :name => _device.fetch(:name, _device.fetch(:display, nil)), + :id => _device_id, + :url => _device.fetch(:url, nil), + :service => _device.fetch(:service, []).map {|s| s.fetch(:name, s.fetch(:display, nil)) }, + :site => _device_site, + :role => ((_role = _device.fetch(:role, nil)) && _role&.has_key?(:name)) ? _role[:name] : _role&.fetch(:display, nil), + :cluster => ((_cluster = _device.fetch(:cluster, nil)) && _cluster&.has_key?(:name)) ? _cluster[:name] : _cluster&.fetch(:display, nil), + :device_type => ((_dtype = _device.fetch(:device_type, nil)) && _dtype&.has_key?(:name)) ? _dtype[:name] : _dtype&.fetch(:display, nil), + :manufacturer => ((_manuf = _device.dig(:device_type, :manufacturer)) && _manuf&.has_key?(:name)) ? _manuf[:name] : _manuf&.fetch(:display, nil), + :details => _verbose ? _device : nil } + end + end + _query[:offset] += _tmp_ip_addresses.length() + break unless (_tmp_ip_addresses.length() >= _page_size) else + # weird/bad response, bail + _exception_error = true break end - end - _device[:service] = _services + end # while true + rescue Faraday::Error + # give up aka do nothing + _exception_error = true end - # non-verbose output is flatter with just names { :name => "name", :id => "id", ... } - # if _verbose, include entire object as :details - _devices << { :name => _device.fetch(:name, _device.fetch(:display, nil)), - :id => _device_id, - :url => _device.fetch(:url, nil), - :service => _device.fetch(:service, []).map {|s| s.fetch(:name, s.fetch(:display, nil)) }, - :site => _device_site, - :role => ((_role = _device.fetch(:role, nil)) && _role&.has_key?(:name)) ? _role[:name] : _role&.fetch(:display, nil), - :cluster => ((_cluster = _device.fetch(:cluster, nil)) && _cluster&.has_key?(:name)) ? _cluster[:name] : _cluster&.fetch(:display, nil), - :device_type => ((_dtype = _device.fetch(:device_type, nil)) && _dtype&.has_key?(:name)) ? _dtype[:name] : _dtype&.fetch(:display, nil), - :manufacturer => ((_manuf = _device.dig(:device_type, :manufacturer)) && _manuf&.has_key?(:name)) ? _manuf[:name] : _manuf&.fetch(:display, nil), - :details => _verbose ? _device : nil } - end - end - _query[:offset] += _tmp_ip_addresses.length() - break unless (_tmp_ip_addresses.length() >= _page_size) - else - # weird/bad response, bail - _exception_error_general = true - break - end - end # while true - rescue Faraday::Error - # give up aka do nothing - _exception_error_general = true - end - - if _autopopulate && (_query[:offset] == 0) && !_exception_error_general && !_exception_error_connection && _key_ip&.private? - - # no results found, autopopulate enabled, private-space IP address... - # let's create an entry for this device - - # if MAC is set but OUI is not, do a quick lookup - if (!_autopopulate_mac.nil? && !_autopopulate_mac.empty?) && - (_autopopulate_oui.nil? || _autopopulate_oui.empty?) - then - case _autopopulate_mac - when String - if @macregex.match?(_autopopulate_mac) - _macint = mac_string_to_integer(_autopopulate_mac) - _vendor = @macarray.bsearch{ |_vendormac| (_macint < _vendormac[0]) ? -1 : ((_macint > _vendormac[1]) ? 1 : 0)} - _autopopulate_oui = _vendor[2] unless _vendor.nil? - end # _autopopulate_mac matches @macregex - when Array - _autopopulate_mac.each do |_addr| - if @macregex.match?(_addr) - _macint = mac_string_to_integer(_addr) - _vendor = @macarray.bsearch{ |_vendormac| (_macint < _vendormac[0]) ? -1 : ((_macint > _vendormac[1]) ? 1 : 0)} - if !_vendor.nil? - _autopopulate_oui = _vendor[2] - break - end # !_vendor.nil? - end # _addr matches @macregex - end # _autopopulate_mac.each do - end # case statement _autopopulate_mac String vs. Array - end # MAC is populated but OUI is not - - # match/look up manufacturer based on OUI - if !_autopopulate_oui.nil? && !_autopopulate_oui.empty? - - _autopopulate_oui = _autopopulate_oui.first() unless !_autopopulate_oui.is_a?(Array) - - # does it look like a VM or a regular device? - if @vm_namesarray.include?(_autopopulate_oui.downcase) - # looks like this is probably a virtual machine - _autopopulate_manuf = { :name => _autopopulate_oui, - :match => 1.0, - :vm => true, - :id => nil } - - else - # looks like this is not a virtual machine (or we can't tell) so assume its' a regular device - _autopopulate_manuf = @manuf_hash.getset(_autopopulate_oui) { - _fuzzy_matcher = FuzzyStringMatch::JaroWinkler.create( :pure ) - _manufs = Array.new - # fetch the manufacturers to do the comparison. this is a lot of work - # and not terribly fast but once the hash it populated it shouldn't happen too often - _query = { :offset => 0, - :limit => _page_size } - begin - while true do - if (_manufs_response = @netbox_conn.get('dcim/manufacturers/', _query).body) && - _manufs_response.is_a?(Hash) + + if _autopopulate && (_query[:offset] == 0) && !_exception_error && _key_ip&.private? + + # no results found, autopopulate enabled, private-space IP address... + # let's create an entry for this device + + # if MAC is set but OUI is not, do a quick lookup + if (!_autopopulate_mac.nil? && !_autopopulate_mac.empty?) && + (_autopopulate_oui.nil? || _autopopulate_oui.empty?) then - _tmp_manufs = _manufs_response.fetch(:results, []) - _tmp_manufs.each do |_manuf| - _tmp_name = _manuf.fetch(:name, _manuf.fetch(:display, nil)) - _manufs << { :name => _tmp_name, - :id => _manuf.fetch(:id, nil), - :url => _manuf.fetch(:url, nil), - :match => _fuzzy_matcher.getDistance(_tmp_name.to_s.downcase, _autopopulate_oui.to_s.downcase), - :vm => false - } + case _autopopulate_mac + when String + if @macregex.match?(_autopopulate_mac) + _macint = mac_string_to_integer(_autopopulate_mac) + _vendor = @macarray.bsearch{ |_vendormac| (_macint < _vendormac[0]) ? -1 : ((_macint > _vendormac[1]) ? 1 : 0)} + _autopopulate_oui = _vendor[2] unless _vendor.nil? + end # _autopopulate_mac matches @macregex + when Array + _autopopulate_mac.each do |_addr| + if @macregex.match?(_addr) + _macint = mac_string_to_integer(_addr) + _vendor = @macarray.bsearch{ |_vendormac| (_macint < _vendormac[0]) ? -1 : ((_macint > _vendormac[1]) ? 1 : 0)} + if !_vendor.nil? + _autopopulate_oui = _vendor[2] + break + end # !_vendor.nil? + end # _addr matches @macregex + end # _autopopulate_mac.each do + end # case statement _autopopulate_mac String vs. Array + end # MAC is populated but OUI is not + + # match/look up manufacturer based on OUI + if !_autopopulate_oui.nil? && !_autopopulate_oui.empty? + + _autopopulate_oui = _autopopulate_oui.first() unless !_autopopulate_oui.is_a?(Array) + + # does it look like a VM or a regular device? + if @vm_namesarray.include?(_autopopulate_oui.downcase) + # looks like this is probably a virtual machine + _autopopulate_manuf = { :name => _autopopulate_oui, + :match => 1.0, + :vm => true, + :id => nil } + + else + # looks like this is not a virtual machine (or we can't tell) so assume its' a regular device + _autopopulate_manuf = @manuf_hash.getset(_autopopulate_oui) { + _fuzzy_matcher = FuzzyStringMatch::JaroWinkler.create( :pure ) + _manufs = Array.new + # fetch the manufacturers to do the comparison. this is a lot of work + # and not terribly fast but once the hash it populated it shouldn't happen too often + _query = { :offset => 0, + :limit => _page_size } + begin + while true do + if (_manufs_response = _nb.get('dcim/manufacturers/', _query).body) && + _manufs_response.is_a?(Hash) + then + _tmp_manufs = _manufs_response.fetch(:results, []) + _tmp_manufs.each do |_manuf| + _tmp_name = _manuf.fetch(:name, _manuf.fetch(:display, nil)) + _manufs << { :name => _tmp_name, + :id => _manuf.fetch(:id, nil), + :url => _manuf.fetch(:url, nil), + :match => _fuzzy_matcher.getDistance(_tmp_name.to_s.downcase, _autopopulate_oui.to_s.downcase), + :vm => false + } + end + _query[:offset] += _tmp_manufs.length() + break unless (_tmp_manufs.length() >= _page_size) + else + break + end + end + rescue Faraday::Error + # give up aka do nothing + _exception_error = true + end + # return the manuf with the highest match + !_manufs&.empty? ? _manufs.max_by{|k| k[:match] } : nil + } + end # virtual machine vs. regular device + end # _autopopulate_oui specified + + if !_autopopulate_manuf.is_a?(Hash) + # no match was found at ANY match level (empty database or no OUI specified), set default ("unspecified") manufacturer + _autopopulate_manuf = { :name => _autopopulate_create_manuf ? _autopopulate_oui : _autopopulate_default_manuf, + :match => 0.0, + :vm => false, + :id => nil} + end + + # make sure the site and role exists + + _autopopulate_site = @site_hash.getset(_autopopulate_default_site) { + begin + _site = nil + + # look it up first + _query = { :offset => 0, + :limit => 1, + :name => _autopopulate_default_site } + if (_sites_response = _nb.get('dcim/sites/', _query).body) && + _sites_response.is_a?(Hash) && + (_tmp_sites = _sites_response.fetch(:results, [])) && + (_tmp_sites.length() > 0) + then + _site = _tmp_sites.first + end + + if _site.nil? + # the device site is not found, create it + _site_data = { :name => _autopopulate_default_site, + :slug => _autopopulate_default_site.to_url, + :status => "active" } + if (_site_create_response = _nb.post('dcim/sites/', _site_data.to_json, _nb_headers).body) && + _site_create_response.is_a?(Hash) && + _site_create_response.has_key?(:id) + then + _site = _site_create_response + end + end + + rescue Faraday::Error + # give up aka do nothing + _exception_error = true end - _query[:offset] += _tmp_manufs.length() - break unless (_tmp_manufs.length() >= _page_size) - else - break + _site + } + + _autopopulate_role = @role_hash.getset(_autopopulate_default_role) { + begin + _role = nil + + # look it up first + _query = { :offset => 0, + :limit => 1, + :name => _autopopulate_default_role } + if (_roles_response = _nb.get('dcim/device-roles/', _query).body) && + _roles_response.is_a?(Hash) && + (_tmp_roles = _roles_response.fetch(:results, [])) && + (_tmp_roles.length() > 0) + then + _role = _tmp_roles.first + end + + if _role.nil? + # the role is not found, create it + _role_data = { :name => _autopopulate_default_role, + :slug => _autopopulate_default_role.to_url, + :color => "d3d3d3" } + if (_role_create_response = _nb.post('dcim/device-roles/', _role_data.to_json, _nb_headers).body) && + _role_create_response.is_a?(Hash) && + _role_create_response.has_key?(:id) + then + _role = _role_create_response + end + end + + rescue Faraday::Error + # give up aka do nothing + _exception_error = true + end + _role + } + + # we should have found or created the autopopulate role and site + begin + if _autopopulate_site&.fetch(:id, nil)&.nonzero? && + _autopopulate_role&.fetch(:id, nil)&.nonzero? + then + + if _autopopulate_manuf[:vm] + # a virtual machine + _device_name = _autopopulate_hostname.to_s.empty? ? "#{_autopopulate_manuf[:name]} @ #{_key}" : "#{_autopopulate_hostname} @ #{_key}" + _device_data = { :name => _device_name, + :site => _autopopulate_site[:id], + :status => "staged" } + if (_device_create_response = _nb.post('virtualization/virtual-machines/', _device_data.to_json, _nb_headers).body) && + _device_create_response.is_a?(Hash) && + _device_create_response.has_key?(:id) + then + _autopopulate_device = _device_create_response + end + + else + # a regular non-vm device + + if !_autopopulate_manuf.fetch(:id, nil)&.nonzero? + # the manufacturer was default (not found) so look it up first + _query = { :offset => 0, + :limit => 1, + :name => _autopopulate_manuf[:name] } + if (_manufs_response = _nb.get('dcim/manufacturers/', _query).body) && + _manufs_response.is_a?(Hash) && + (_tmp_manufs = _manufs_response.fetch(:results, [])) && + (_tmp_manufs.length() > 0) + then + _autopopulate_manuf[:id] = _tmp_manufs.first.fetch(:id, nil) + _autopopulate_manuf[:match] = 1.0 + end + end + + if !_autopopulate_manuf.fetch(:id, nil)&.nonzero? + # the manufacturer is still not found, create it + _manuf_data = { :name => _autopopulate_manuf[:name], + :slug => _autopopulate_manuf[:name].to_url } + if (_manuf_create_response = _nb.post('dcim/manufacturers/', _manuf_data.to_json, _nb_headers).body) && + _manuf_create_response.is_a?(Hash) + then + _autopopulate_manuf[:id] = _manuf_create_response.fetch(:id, nil) + _autopopulate_manuf[:match] = 1.0 + end + end + + # at this point we *must* have the manufacturer ID + if _autopopulate_manuf.fetch(:id, nil)&.nonzero? + + # make sure the desired device type also exists, look it up first + _query = { :offset => 0, + :limit => 1, + :manufacturer_id => _autopopulate_manuf[:id], + :model => _autopopulate_default_dtype } + if (_dtypes_response = _nb.get('dcim/device-types/', _query).body) && + _dtypes_response.is_a?(Hash) && + (_tmp_dtypes = _dtypes_response.fetch(:results, [])) && + (_tmp_dtypes.length() > 0) + then + _autopopulate_dtype = _tmp_dtypes.first + end + + if _autopopulate_dtype.nil? + # the device type is not found, create it + _dtype_data = { :manufacturer => _autopopulate_manuf[:id], + :model => _autopopulate_default_dtype, + :slug => _autopopulate_default_dtype.to_url } + if (_dtype_create_response = _nb.post('dcim/device-types/', _dtype_data.to_json, _nb_headers).body) && + _dtype_create_response.is_a?(Hash) && + _dtype_create_response.has_key?(:id) + then + _autopopulate_dtype = _dtype_create_response + end + end + + # # now we must also have the device type ID + if _autopopulate_dtype&.fetch(:id, nil)&.nonzero? + + # create the device + _device_name = _autopopulate_hostname.to_s.empty? ? "#{_autopopulate_manuf[:name]} @ #{_key}" : "#{_autopopulate_hostname} @ #{_key}" + _device_data = { :name => _device_name, + :device_type => _autopopulate_dtype[:id], + :role => _autopopulate_role[:id], + :site => _autopopulate_site[:id], + :status => "staged" } + if (_device_create_response = _nb.post('dcim/devices/', _device_data.to_json, _nb_headers).body) && + _device_create_response.is_a?(Hash) && + _device_create_response.has_key?(:id) + then + _autopopulate_device = _device_create_response + end + + end # _autopopulate_dtype[:id] is valid + + end # _autopopulate_manuf[:id] is valid + + end # virtual machine vs. regular device + + end # site and role are valid + + rescue Faraday::Error + # give up aka do nothing + _exception_error = true end - end - rescue Faraday::Error - # give up aka do nothing - _exception_error_general = true - end - # return the manuf with the highest match - !_manufs&.empty? ? _manufs.max_by{|k| k[:match] } : nil - } - end # virtual machine vs. regular device - end # _autopopulate_oui specified - - if !_autopopulate_manuf.is_a?(Hash) - # no match was found at ANY match level (empty database or no OUI specified), set default ("unspecified") manufacturer - _autopopulate_manuf = { :name => _autopopulate_create_manuf ? _autopopulate_oui : _autopopulate_default_manuf, - :match => 0.0, - :vm => false, - :id => nil} - end - - # make sure the site and role exists - - _autopopulate_site = @site_hash.getset(_autopopulate_default_site) { - begin - _site = nil - - # look it up first - _query = { :offset => 0, - :limit => 1, - :name => _autopopulate_default_site } - if (_sites_response = @netbox_conn.get('dcim/sites/', _query).body) && - _sites_response.is_a?(Hash) && - (_tmp_sites = _sites_response.fetch(:results, [])) && - (_tmp_sites.length() > 0) - then - _site = _tmp_sites.first - end - - if _site.nil? - # the device site is not found, create it - _site_data = { :name => _autopopulate_default_site, - :slug => _autopopulate_default_site.to_url, - :status => "active" } - if (_site_create_response = @netbox_conn.post('dcim/sites/', _site_data.to_json, @netbox_headers).body) && - _site_create_response.is_a?(Hash) && - _site_create_response.has_key?(:id) - then - _site = _site_create_response - end - end - - rescue Faraday::Error - # give up aka do nothing - _exception_error_general = true - end - _site - } - - _autopopulate_role = @role_hash.getset(_autopopulate_default_role) { - begin - _role = nil - - # look it up first - _query = { :offset => 0, - :limit => 1, - :name => _autopopulate_default_role } - if (_roles_response = @netbox_conn.get('dcim/device-roles/', _query).body) && - _roles_response.is_a?(Hash) && - (_tmp_roles = _roles_response.fetch(:results, [])) && - (_tmp_roles.length() > 0) - then - _role = _tmp_roles.first - end - - if _role.nil? - # the role is not found, create it - _role_data = { :name => _autopopulate_default_role, - :slug => _autopopulate_default_role.to_url, - :color => "d3d3d3" } - if (_role_create_response = @netbox_conn.post('dcim/device-roles/', _role_data.to_json, @netbox_headers).body) && - _role_create_response.is_a?(Hash) && - _role_create_response.has_key?(:id) - then - _role = _role_create_response - end - end - - rescue Faraday::ConnectionFailed - # give up aka do nothing (and connect next time) - _exception_error_connection = true - rescue Faraday::Error - # give up aka do nothing - _exception_error_general = true - end - _role - } - - # we should have found or created the autopopulate role and site - begin - if _autopopulate_site&.fetch(:id, nil)&.nonzero? && - _autopopulate_role&.fetch(:id, nil)&.nonzero? - then - - if _autopopulate_manuf[:vm] - # a virtual machine - _device_name = _autopopulate_hostname.to_s.empty? ? "#{_autopopulate_manuf[:name]} @ #{_key}" : "#{_autopopulate_hostname} @ #{_key}" - _device_data = { :name => _device_name, - :site => _autopopulate_site[:id], - :status => "staged" } - if (_device_create_response = @netbox_conn.post('virtualization/virtual-machines/', _device_data.to_json, @netbox_headers).body) && - _device_create_response.is_a?(Hash) && - _device_create_response.has_key?(:id) - then - _autopopulate_device = _device_create_response - end - else - # a regular non-vm device - - if !_autopopulate_manuf.fetch(:id, nil)&.nonzero? - # the manufacturer was default (not found) so look it up first - _query = { :offset => 0, - :limit => 1, - :name => _autopopulate_manuf[:name] } - if (_manufs_response = @netbox_conn.get('dcim/manufacturers/', _query).body) && - _manufs_response.is_a?(Hash) && - (_tmp_manufs = _manufs_response.fetch(:results, [])) && - (_tmp_manufs.length() > 0) - then - _autopopulate_manuf[:id] = _tmp_manufs.first.fetch(:id, nil) - _autopopulate_manuf[:match] = 1.0 + if !_autopopulate_device.nil? + # we created a device, so send it back out as the result for the event as well + _devices << { :name => _autopopulate_device&.fetch(:name, _autopopulate_device&.fetch(:display, nil)), + :id => _autopopulate_device&.fetch(:id, nil), + :url => _autopopulate_device&.fetch(:url, nil), + :site => _autopopulate_site&.fetch(:name, nil), + :role => _autopopulate_role&.fetch(:name, nil), + :device_type => _autopopulate_dtype&.fetch(:name, nil), + :manufacturer => _autopopulate_manuf&.fetch(:name, nil), + :details => _verbose ? _autopopulate_device : nil } + end # _autopopulate_device was not nil (i.e., we autocreated a device) + + end # _autopopulate turned on and no results found + + _devices = collect_values(crush(_devices)) + _devices.fetch(:service, [])&.flatten!&.uniq! + _lookup_result = _devices + end # _lookup_type == :ip_device + + # this || is because we are going to need to do the VRF lookup if we're autopopulating + # as well as if we're specifically requested to do that enrichment + + if (_lookup_type == :ip_vrf) || !_autopopulate_device.nil? + ################################################################################# + # retrieve the list VRFs containing IP address prefixes containing the search key + _vrfs = Array.new + _query = { :contains => _key, + :offset => 0, + :limit => _page_size } + _query[:site_n] = _lookup_site unless _lookup_site.nil? || _lookup_site.empty? + begin + while true do + if (_prefixes_response = _nb.get('ipam/prefixes/', _query).body) && + _prefixes_response.is_a?(Hash) + then + _tmp_prefixes = _prefixes_response.fetch(:results, []) + _tmp_prefixes.each do |p| + if (_vrf = p.fetch(:vrf, nil)) + # non-verbose output is flatter with just names { :name => "name", :id => "id", ... } + # if _verbose, include entire object as :details + _vrfs << { :name => _vrf.fetch(:name, _vrf.fetch(:display, nil)), + :id => _vrf.fetch(:id, nil), + :site => ((_site = p.fetch(:site, nil)) && _site&.has_key?(:name)) ? _site[:name] : _site&.fetch(:display, nil), + :tenant => ((_tenant = p.fetch(:tenant, nil)) && _tenant&.has_key?(:name)) ? _tenant[:name] : _tenant&.fetch(:display, nil), + :url => p.fetch(:url, _vrf.fetch(:url, nil)), + :details => _verbose ? _vrf.merge({:prefix => p.tap { |h| h.delete(:vrf) }}) : nil } + end + end + _query[:offset] += _tmp_prefixes.length() + break unless (_tmp_prefixes.length() >= _page_size) + else + break + end + end + rescue Faraday::Error + # give up aka do nothing + _exception_error = true end - end - - if !_autopopulate_manuf.fetch(:id, nil)&.nonzero? - # the manufacturer is still not found, create it - _manuf_data = { :name => _autopopulate_manuf[:name], - :slug => _autopopulate_manuf[:name].to_url } - if (_manuf_create_response = @netbox_conn.post('dcim/manufacturers/', _manuf_data.to_json, @netbox_headers).body) && - _manuf_create_response.is_a?(Hash) - then - _autopopulate_manuf[:id] = _manuf_create_response.fetch(:id, nil) - _autopopulate_manuf[:match] = 1.0 + _vrfs = collect_values(crush(_vrfs)) + _lookup_result = _vrfs unless (_lookup_type != :ip_vrf) + end # _lookup_type == :ip_vrf + + if !_autopopulate_device.nil? && _autopopulate_device.fetch(:id, nil)&.nonzero? + # device has been created, we need to create an interface for it + _interface_data = { _autopopulate_manuf[:vm] ? :virtual_machine : :device => _autopopulate_device[:id], + :name => "e0", + :type => "other" } + if !_autopopulate_mac.nil? && !_autopopulate_mac.empty? + _interface_data[:mac_address] = _autopopulate_mac.is_a?(Array) ? _autopopulate_mac.first : _autopopulate_mac end - end - - # at this point we *must* have the manufacturer ID - if _autopopulate_manuf.fetch(:id, nil)&.nonzero? - - # make sure the desired device type also exists, look it up first - _query = { :offset => 0, - :limit => 1, - :manufacturer_id => _autopopulate_manuf[:id], - :model => _autopopulate_default_dtype } - if (_dtypes_response = @netbox_conn.get('dcim/device-types/', _query).body) && - _dtypes_response.is_a?(Hash) && - (_tmp_dtypes = _dtypes_response.fetch(:results, [])) && - (_tmp_dtypes.length() > 0) + if !_vrfs.nil? && !_vrfs.empty? + _interface_data[:vrf] = _vrfs.fetch(:id, []).first + end + if (_interface_create_reponse = _nb.post(_autopopulate_manuf[:vm] ? 'virtualization/interfaces/' : 'dcim/interfaces/', _interface_data.to_json, _nb_headers).body) && + _interface_create_reponse.is_a?(Hash) && + _interface_create_reponse.has_key?(:id) then - _autopopulate_dtype = _tmp_dtypes.first + _autopopulate_interface = _interface_create_reponse end - if _autopopulate_dtype.nil? - # the device type is not found, create it - _dtype_data = { :manufacturer => _autopopulate_manuf[:id], - :model => _autopopulate_default_dtype, - :slug => _autopopulate_default_dtype.to_url } - if (_dtype_create_response = @netbox_conn.post('dcim/device-types/', _dtype_data.to_json, @netbox_headers).body) && - _dtype_create_response.is_a?(Hash) && - _dtype_create_response.has_key?(:id) + if !_autopopulate_interface.nil? && _autopopulate_interface.fetch(:id, nil)&.nonzero? + # interface has been created, we need to create an IP address for it + _ip_data = { :address => "#{_key}/#{_key_ip&.prefix()}", + :assigned_object_type => _autopopulate_manuf[:vm] ? "virtualization.vminterface" : "dcim.interface", + :assigned_object_id => _autopopulate_interface[:id], + :status => "active" } + if (_vrf = _autopopulate_interface.fetch(:vrf, nil)) && + (_vrf.has_key?(:id)) then - _autopopulate_dtype = _dtype_create_response + _ip_data[:vrf] = _vrf[:id] end - end - - # # now we must also have the device type ID - if _autopopulate_dtype&.fetch(:id, nil)&.nonzero? - - # create the device - _device_name = _autopopulate_hostname.to_s.empty? ? "#{_autopopulate_manuf[:name]} @ #{_key}" : "#{_autopopulate_hostname} @ #{_key}" - _device_data = { :name => _device_name, - :device_type => _autopopulate_dtype[:id], - :role => _autopopulate_role[:id], - :site => _autopopulate_site[:id], - :status => "staged" } - if (_device_create_response = @netbox_conn.post('dcim/devices/', _device_data.to_json, @netbox_headers).body) && - _device_create_response.is_a?(Hash) && - _device_create_response.has_key?(:id) + if (_ip_create_reponse = _nb.post('ipam/ip-addresses/', _ip_data.to_json, _nb_headers).body) && + _ip_create_reponse.is_a?(Hash) && + _ip_create_reponse.has_key?(:id) then - _autopopulate_device = _device_create_response + _autopopulate_ip = _ip_create_reponse end + end # check if interface was created and has ID + + if !_autopopulate_ip.nil? && _autopopulate_ip.fetch(:id, nil)&.nonzero? + # IP address was created, need to associate it as the primary IP for the device + _primary_ip_data = { _key_ip&.ipv6? ? :primary_ip6 : :primary_ip4 => _autopopulate_ip[:id] } + if (_ip_primary_reponse = _nb.patch("#{_autopopulate_manuf[:vm] ? 'virtualization/virtual-machines' : 'dcim/devices'}/#{_autopopulate_device[:id]}/", _primary_ip_data.to_json, _nb_headers).body) && + _ip_primary_reponse.is_a?(Hash) && + _ip_primary_reponse.has_key?(:id) + then + _autopopulate_device = _ip_create_reponse + end + end # check if the IP address was created and has an ID - end # _autopopulate_dtype[:id] is valid - - end # _autopopulate_manuf[:id] is valid - - end # virtual machine vs. regular device - - end # site and role are valid - - rescue Faraday::Error - # give up aka do nothing - _exception_error_general = true - end - - if !_autopopulate_device.nil? - # we created a device, so send it back out as the result for the event as well - _devices << { :name => _autopopulate_device&.fetch(:name, _autopopulate_device&.fetch(:display, nil)), - :id => _autopopulate_device&.fetch(:id, nil), - :url => _autopopulate_device&.fetch(:url, nil), - :site => _autopopulate_site&.fetch(:name, nil), - :role => _autopopulate_role&.fetch(:name, nil), - :device_type => _autopopulate_dtype&.fetch(:name, nil), - :manufacturer => _autopopulate_manuf&.fetch(:name, nil), - :details => _verbose ? _autopopulate_device : nil } - end # _autopopulate_device was not nil (i.e., we autocreated a device) - - end # _autopopulate turned on and no results found - - _devices = collect_values(crush(_devices)) - _devices.fetch(:service, [])&.flatten!&.uniq! - _result = _devices - end # _lookup_type == :ip_device - - # this || is because we are going to need to do the VRF lookup if we're autopopulating - # as well as if we're specifically requested to do that enrichment - - if (_lookup_type == :ip_vrf) || !_autopopulate_device.nil? - ################################################################################# - # retrieve the list VRFs containing IP address prefixes containing the search key - _vrfs = Array.new - _query = { :contains => _key, - :offset => 0, - :limit => _page_size } - _query[:site_n] = _lookup_site unless _lookup_site.nil? || _lookup_site.empty? - begin - while true do - if (_prefixes_response = @netbox_conn.get('ipam/prefixes/', _query).body) && - _prefixes_response.is_a?(Hash) - then - _tmp_prefixes = _prefixes_response.fetch(:results, []) - _tmp_prefixes.each do |p| - if (_vrf = p.fetch(:vrf, nil)) - # non-verbose output is flatter with just names { :name => "name", :id => "id", ... } - # if _verbose, include entire object as :details - _vrfs << { :name => _vrf.fetch(:name, _vrf.fetch(:display, nil)), - :id => _vrf.fetch(:id, nil), - :site => ((_site = p.fetch(:site, nil)) && _site&.has_key?(:name)) ? _site[:name] : _site&.fetch(:display, nil), - :tenant => ((_tenant = p.fetch(:tenant, nil)) && _tenant&.has_key?(:name)) ? _tenant[:name] : _tenant&.fetch(:display, nil), - :url => p.fetch(:url, _vrf.fetch(:url, nil)), - :details => _verbose ? _vrf.merge({:prefix => p.tap { |h| h.delete(:vrf) }}) : nil } - end - end - _query[:offset] += _tmp_prefixes.length() - break unless (_tmp_prefixes.length() >= _page_size) - else - break - end - end - rescue Faraday::Error - # give up aka do nothing - _exception_error_general = true - end - _vrfs = collect_values(crush(_vrfs)) - _result = _vrfs unless (_lookup_type != :ip_vrf) - end # _lookup_type == :ip_vrf - - if !_autopopulate_device.nil? && _autopopulate_device.fetch(:id, nil)&.nonzero? - # device has been created, we need to create an interface for it - _interface_data = { _autopopulate_manuf[:vm] ? :virtual_machine : :device => _autopopulate_device[:id], - :name => "e0", - :type => "other" } - if !_autopopulate_mac.nil? && !_autopopulate_mac.empty? - _interface_data[:mac_address] = _autopopulate_mac.is_a?(Array) ? _autopopulate_mac.first : _autopopulate_mac - end - if !_vrfs.nil? && !_vrfs.empty? - _interface_data[:vrf] = _vrfs.fetch(:id, []).first - end - if (_interface_create_reponse = @netbox_conn.post(_autopopulate_manuf[:vm] ? 'virtualization/interfaces/' : 'dcim/interfaces/', _interface_data.to_json, @netbox_headers).body) && - _interface_create_reponse.is_a?(Hash) && - _interface_create_reponse.has_key?(:id) - then - _autopopulate_interface = _interface_create_reponse - end - - if !_autopopulate_interface.nil? && _autopopulate_interface.fetch(:id, nil)&.nonzero? - # interface has been created, we need to create an IP address for it - _ip_data = { :address => "#{_key}/#{_key_ip&.prefix()}", - :assigned_object_type => _autopopulate_manuf[:vm] ? "virtualization.vminterface" : "dcim.interface", - :assigned_object_id => _autopopulate_interface[:id], - :status => "active" } - if (_vrf = _autopopulate_interface.fetch(:vrf, nil)) && - (_vrf.has_key?(:id)) - then - _ip_data[:vrf] = _vrf[:id] - end - if (_ip_create_reponse = @netbox_conn.post('ipam/ip-addresses/', _ip_data.to_json, @netbox_headers).body) && - _ip_create_reponse.is_a?(Hash) && - _ip_create_reponse.has_key?(:id) - then - _autopopulate_ip = _ip_create_reponse - end - end # check if interface was created and has ID - - if !_autopopulate_ip.nil? && _autopopulate_ip.fetch(:id, nil)&.nonzero? - # IP address was created, need to associate it as the primary IP for the device - _primary_ip_data = { _key_ip&.ipv6? ? :primary_ip6 : :primary_ip4 => _autopopulate_ip[:id] } - if (_ip_primary_reponse = @netbox_conn.patch("#{_autopopulate_manuf[:vm] ? 'virtualization/virtual-machines' : 'dcim/devices'}/#{_autopopulate_device[:id]}/", _primary_ip_data.to_json, @netbox_headers).body) && - _ip_primary_reponse.is_a?(Hash) && - _ip_primary_reponse.has_key?(:id) - then - _autopopulate_device = _ip_create_reponse - end - end # check if the IP address was created and has an ID - - end # check if device was created and has ID - - if _exception_error_connection && !@netbox_conn_resetting - @netbox_conn_lock.release_read_lock - @netbox_conn_lock.acquire_write_lock - begin - if !@netbox_conn_resetting - @netbox_conn_needs_reset = true - end - ensure - @netbox_conn_lock.release_write_lock - @netbox_conn_lock.acquire_read_lock - end - end - ensure - @netbox_conn_lock.release_read_lock - end + end # check if device was created and has ID + + # yield return value for cache_hash getset + _lookup_result + } if !_result.nil? && _result.has_key?(:url) && !_result[:url]&.empty? _result[:url].map! { |u| u.delete_prefix(@netbox_url_base).gsub('/api/', '/') }