smart_proxy_dhcp_infoblox 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +15 -10
- data/config/dhcp_infoblox.yml.example +17 -0
- data/lib/smart_proxy_dhcp_infoblox.rb +9 -3
- data/lib/smart_proxy_dhcp_infoblox/common_crud.rb +75 -0
- data/lib/smart_proxy_dhcp_infoblox/dhcp_infoblox_main.rb +39 -225
- data/lib/smart_proxy_dhcp_infoblox/dhcp_infoblox_plugin.rb +9 -17
- data/lib/smart_proxy_dhcp_infoblox/dhcp_infoblox_version.rb +1 -1
- data/lib/smart_proxy_dhcp_infoblox/fixed_address_crud.rb +53 -0
- data/lib/smart_proxy_dhcp_infoblox/grid_restart.rb +31 -0
- data/lib/smart_proxy_dhcp_infoblox/host_ipv4_address_crud.rb +60 -0
- data/lib/smart_proxy_dhcp_infoblox/ip_address_arithmetic.rb +34 -0
- data/lib/smart_proxy_dhcp_infoblox/network_address_range_regex_generator.rb +131 -0
- data/lib/smart_proxy_dhcp_infoblox/plugin_configuration.rb +37 -0
- data/lib/smart_proxy_dhcp_infoblox/record_type_validator.rb +8 -0
- data/lib/smart_proxy_dhcp_infoblox/unused_ips.rb +47 -0
- data/test/host_and_fixedaddress_crud_test.rb +169 -0
- data/test/infoblox_provider_test.rb +61 -0
- data/test/plugin_configuration_test.rb +73 -0
- data/test/record_type_validator_test.rb +20 -0
- data/test/regex_generator_test.rb +86 -0
- data/test/test_helper.rb +0 -2
- data/test/unused_ip_test.rb +48 -0
- metadata +25 -49
- data/config/dhcp_infoblox.yml +0 -14
- data/lib/smart_proxy_dhcp_infoblox/dhcp_infoblox_dependencies.rb +0 -5
- data/test/dhcp_infoblox_record_test.rb +0 -95
@@ -1,24 +1,16 @@
|
|
1
|
-
require 'smart_proxy_dhcp_infoblox/dhcp_infoblox_version'
|
2
|
-
|
3
1
|
module Proxy::DHCP::Infoblox
|
4
2
|
class Plugin < ::Proxy::Provider
|
5
|
-
plugin :dhcp_infoblox, ::Proxy::DHCP::Infoblox::VERSION
|
3
|
+
plugin :dhcp_infoblox, ::Proxy::DHCP::Infoblox::VERSION
|
4
|
+
|
5
|
+
default_settings :record_type => 'host', :range => false
|
6
|
+
validate_presence :username, :password
|
6
7
|
|
7
|
-
|
8
|
-
# An exception will be raised if they are initialized with nil values.
|
9
|
-
# Settings not listed under default_settings are considered optional and by default have nil value.
|
10
|
-
default_settings :infoblox_user => 'infoblox',
|
11
|
-
:infoblox_pw => 'infoblox',
|
12
|
-
:infoblox_host => 'infoblox.my.domain',
|
13
|
-
:record_type => 'host',
|
14
|
-
:wapi_version => '2.0',
|
15
|
-
:range => false
|
8
|
+
requires :dhcp, '>= 1.13'
|
16
9
|
|
17
|
-
|
10
|
+
load_classes ::Proxy::DHCP::Infoblox::PluginConfiguration
|
11
|
+
load_validators :record_type_validator => ::Proxy::DHCP::Infoblox::RecordTypeValidator
|
12
|
+
load_dependency_injection_wirings ::Proxy::DHCP::Infoblox::PluginConfiguration
|
18
13
|
|
19
|
-
|
20
|
-
require 'smart_proxy_dhcp_infoblox/dhcp_infoblox_main'
|
21
|
-
require 'smart_proxy_dhcp_infoblox/dhcp_infoblox_dependencies'
|
22
|
-
end
|
14
|
+
validate :record_type, :record_type_validator => true
|
23
15
|
end
|
24
16
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'smart_proxy_dhcp_infoblox/common_crud'
|
2
|
+
|
3
|
+
module ::Proxy::DHCP::Infoblox
|
4
|
+
class FixedAddressCRUD < CommonCRUD
|
5
|
+
def initialize(connection)
|
6
|
+
@memoized_host = nil
|
7
|
+
@memoized_condition = nil
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def all_hosts(subnet_address)
|
12
|
+
network = ::Infoblox::Fixedaddress.find(@connection, 'network' => subnet_address, '_max_results' => 2147483646) #2**(32-cidr_to_i(subnet_address)))
|
13
|
+
network.map {|h| build_reservation(h.name, h, subnet_address)}.compact
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_record_by_ip(subnet_address, ip_address)
|
17
|
+
found = find_host('ipv4addr' => ip_address)
|
18
|
+
return nil if found.nil?
|
19
|
+
build_reservation(found.name, found, subnet_address)
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_record_by_mac(subnet_address, mac_address)
|
23
|
+
found = find_host('mac' => mac_address)
|
24
|
+
return nil if found.nil?
|
25
|
+
build_reservation(found.name, found, subnet_address)
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_host(condition)
|
29
|
+
return @memoized_host if (!@memoized_host.nil? && @memoized_condition == condition)
|
30
|
+
@memoized_condition = condition
|
31
|
+
@memoized_host = ::Infoblox::Fixedaddress.find(@connection, condition.merge('_max_results' => 1)).first
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_host_and_name_by_ip(ip_address)
|
35
|
+
h = find_host('ipv4addr' => ip_address)
|
36
|
+
[h.name, h]
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_host(options)
|
40
|
+
host = ::Infoblox::Fixedaddress.new(:connection => @connection)
|
41
|
+
host.name = options[:hostname]
|
42
|
+
host.ipv4addr = options[:ip]
|
43
|
+
host.mac = options[:mac]
|
44
|
+
# TODO: nextserver, use_nextserver, bootfile, and use_bootfile attrs exist but are not available in the model
|
45
|
+
# Might be useful to extend the model to include these
|
46
|
+
#host.nextserver = options[:nextServer]
|
47
|
+
#host.use_nextserver = true
|
48
|
+
#host.bootfile = options[:filename]
|
49
|
+
#host.use_bootfile = true
|
50
|
+
host
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ::Proxy::DHCP::Infoblox
|
2
|
+
class GridRestart
|
3
|
+
MAX_ATTEMPTS = 3
|
4
|
+
|
5
|
+
include ::Proxy::Log
|
6
|
+
attr_reader :connection
|
7
|
+
|
8
|
+
def initialize(connection)
|
9
|
+
@connection = connection
|
10
|
+
end
|
11
|
+
|
12
|
+
def try_restart
|
13
|
+
logger.debug 'Restarting grid.'
|
14
|
+
|
15
|
+
MAX_ATTEMPTS.times do |tries|
|
16
|
+
sleep tries
|
17
|
+
return if restart
|
18
|
+
end
|
19
|
+
|
20
|
+
logger.info 'Restarting Grid failed.'
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def restart
|
25
|
+
(@grid ||= ::Infoblox::Grid.get(@connection).first).restartservices
|
26
|
+
true
|
27
|
+
rescue Exception => e
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'smart_proxy_dhcp_infoblox/common_crud'
|
2
|
+
require 'smart_proxy_dhcp_infoblox/network_address_range_regex_generator'
|
3
|
+
|
4
|
+
module ::Proxy::DHCP::Infoblox
|
5
|
+
class HostIpv4AddressCRUD < CommonCRUD
|
6
|
+
def initialize(connection)
|
7
|
+
@memoized_host = nil
|
8
|
+
@memoized_condition = nil
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def all_hosts(subnet_address)
|
13
|
+
address_range_regex = NetworkAddressesRegularExpressionGenerator.new.generate_regex(subnet_address)
|
14
|
+
|
15
|
+
hosts = ::Infoblox::Host.find(
|
16
|
+
@connection,
|
17
|
+
'ipv4addr~' => address_range_regex,
|
18
|
+
'_max_results' => 2147483646)
|
19
|
+
|
20
|
+
ip_addr_matcher = Regexp.new(address_range_regex) # pre-compile the regex
|
21
|
+
hosts.map {|host| build_reservation(host.name, host.ipv4addrs.find {|ip| ip_addr_matcher =~ ip.ipv4addr}, subnet_address)}.compact
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_record_by_ip(subnet_address, ip_address)
|
25
|
+
found = find_host('ipv4addr' => ip_address)
|
26
|
+
return nil if found.nil?
|
27
|
+
build_reservation(found.name, found.ipv4addrs.find {|ip| ip.ipv4addr == ip_address}, subnet_address)
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_record_by_mac(subnet_address, mac_address)
|
31
|
+
found = find_host('mac' => mac_address)
|
32
|
+
return nil if found.nil?
|
33
|
+
build_reservation(found.name, found.ipv4addrs.find {|ip| ip.mac == mac_address}, subnet_address)
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_host_and_name_by_ip(ip_address)
|
37
|
+
h = find_host('ipv4addr' => ip_address)
|
38
|
+
[h.name, h.ipv4addrs.find {|ip| ip.ipv4addr == ip_address}]
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_host(condition)
|
42
|
+
return @memoized_host if (!@memoized_host.nil? && @memoized_condition == condition)
|
43
|
+
@memoized_condition = condition
|
44
|
+
@memoized_host = ::Infoblox::Host.find(@connection, condition.merge('_max_results' => 1)).first
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_host(options)
|
48
|
+
host = ::Infoblox::Host.new(:connection => @connection)
|
49
|
+
host.name = options[:hostname]
|
50
|
+
host_addr = host.add_ipv4addr(options[:ip]).last
|
51
|
+
host_addr.mac = options[:mac]
|
52
|
+
host_addr.configure_for_dhcp = true
|
53
|
+
host_addr.nextserver = options[:nextServer]
|
54
|
+
host_addr.use_nextserver = true
|
55
|
+
host_addr.bootfile = options[:filename]
|
56
|
+
host_addr.use_bootfile = true
|
57
|
+
host
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ::Proxy::DHCP::Infoblox
|
2
|
+
module IpAddressArithmetic
|
3
|
+
def cidr_to_ip_mask(prefix_length)
|
4
|
+
bitmask = 0xFFFFFFFF ^ (2 ** (32-prefix_length) - 1)
|
5
|
+
(0..3).map {|i| (bitmask >> i*8) & 0xFF}.reverse.join('.')
|
6
|
+
end
|
7
|
+
|
8
|
+
def ipv4_to_i(an_address)
|
9
|
+
an_address.split('.').inject(0) {|a, c| (a << 8) + c.to_i}
|
10
|
+
end
|
11
|
+
|
12
|
+
def i_to_ipv4(i)
|
13
|
+
(0..3).inject([]) {|a, c| a.push((i >> (c * 8)) & 0xFF)}.reverse.join('.')
|
14
|
+
end
|
15
|
+
|
16
|
+
def cidr_to_bitmask(prefix_length)
|
17
|
+
0xFFFFFFFF ^ (2 ** (32-prefix_length) - 1)
|
18
|
+
end
|
19
|
+
|
20
|
+
def cidr_to_i(an_address_with_cidr)
|
21
|
+
an_address_with_cidr.split("/").last.to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
def network_cidr_to_range(network_and_cidr)
|
25
|
+
network_addr, cidr = network_and_cidr.split('/')
|
26
|
+
mask = cidr_to_bitmask(cidr.to_i)
|
27
|
+
|
28
|
+
range_start = ipv4_to_i(network_addr) & mask
|
29
|
+
range_end = range_start | (0xFFFFFFFF ^ mask)
|
30
|
+
|
31
|
+
[i_to_ipv4(range_start), i_to_ipv4(range_end)]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'smart_proxy_dhcp_infoblox/ip_address_arithmetic'
|
2
|
+
|
3
|
+
module ::Proxy::DHCP::Infoblox
|
4
|
+
class RangeRegularExpressionGenerator
|
5
|
+
class Node
|
6
|
+
attr_accessor :value, :children
|
7
|
+
|
8
|
+
def initialize(value, children = [])
|
9
|
+
@value = value
|
10
|
+
@children = children
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_children(values)
|
14
|
+
return if values.empty?
|
15
|
+
node = (found = children.find {|n| n.value == values.first}).nil? ? add_child(Node.new(values.first)) : found
|
16
|
+
node.add_children(values[1..-1])
|
17
|
+
end
|
18
|
+
|
19
|
+
def <=>(other)
|
20
|
+
return -1 if value.to_s == '0?'
|
21
|
+
return 1 if other.value.to_s == '0?'
|
22
|
+
return 0 if value == other.value
|
23
|
+
return -1 if value < other.value
|
24
|
+
1
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_child(a_node)
|
28
|
+
children.push(a_node)
|
29
|
+
children.sort!
|
30
|
+
a_node
|
31
|
+
end
|
32
|
+
|
33
|
+
def group_children
|
34
|
+
children.each {|n| n.group_children}
|
35
|
+
return if children.size < 2
|
36
|
+
@children = children[1..-1].inject([MergedNode.new(children.first)]) do |grouped, to_group|
|
37
|
+
current = MergedNode.new(to_group)
|
38
|
+
found = grouped.find {|g| ((g.value != ['0?'] && current.value != ['0?']) || (current.value == ['0?'] && g.value == ['0?'])) && (g.children == current.children)}
|
39
|
+
found.nil? ? grouped.push(current) : found.merge(current)
|
40
|
+
grouped
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def as_regex
|
45
|
+
children.empty? ? [value.to_s] : children.map {|c| c.as_regex.map {|r| value.to_s + r}}.flatten
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class MergedNode
|
50
|
+
attr_accessor :value, :children
|
51
|
+
def initialize(a_node)
|
52
|
+
@value = [a_node.value].flatten
|
53
|
+
@children = a_node.children
|
54
|
+
end
|
55
|
+
|
56
|
+
def merge(other)
|
57
|
+
value.push(other.value).flatten!
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def as_regex
|
62
|
+
children.empty? ? [value_as_regex] : children.map {|c| c.as_regex.map {|r| value_as_regex + r}}.flatten
|
63
|
+
end
|
64
|
+
|
65
|
+
def value_as_regex
|
66
|
+
value.size < 2 ? value.first.to_s : "[#{value.join('')}]"
|
67
|
+
end
|
68
|
+
|
69
|
+
def ==(other)
|
70
|
+
return false if self.class != other.class
|
71
|
+
value == other.value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Root < Node
|
76
|
+
def add_number(a_number)
|
77
|
+
add_children((['0?', '0?'] + digits(a_number))[-3, 3])
|
78
|
+
end
|
79
|
+
|
80
|
+
def as_regex
|
81
|
+
group_children
|
82
|
+
"(%s)" % children.map {|c| c.as_regex}.join('|')
|
83
|
+
end
|
84
|
+
|
85
|
+
def digits(a_number)
|
86
|
+
to_return = []
|
87
|
+
begin
|
88
|
+
to_return.push(a_number % 10)
|
89
|
+
a_number = a_number / 10
|
90
|
+
end while a_number != 0
|
91
|
+
to_return.reverse
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def range_regex(range_start, range_end)
|
96
|
+
root = Root.new(nil)
|
97
|
+
(range_start..range_end).to_a.each {|i| root.add_number(i)}
|
98
|
+
root.as_regex
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class NetworkAddressesRegularExpressionGenerator
|
103
|
+
include IpAddressArithmetic
|
104
|
+
|
105
|
+
def generate_regex(network_and_cidr)
|
106
|
+
range_to_regex(network_cidr_range_octets(network_and_cidr))
|
107
|
+
end
|
108
|
+
|
109
|
+
def network_cidr_range_octets(network_and_cidr)
|
110
|
+
range = network_cidr_to_range(network_and_cidr)
|
111
|
+
range_start_octets = range.first.split('.').map(&:to_i)
|
112
|
+
range_end_octets = range.last.split('.').map(&:to_i)
|
113
|
+
|
114
|
+
(0..3).map {|i| [range_start_octets[i], range_end_octets[i]]}
|
115
|
+
end
|
116
|
+
|
117
|
+
def range_to_regex(range)
|
118
|
+
range.inject([]) do |a, c|
|
119
|
+
start_range, end_range = c
|
120
|
+
regex = if start_range == end_range
|
121
|
+
start_range.to_s
|
122
|
+
elsif start_range == 0 && end_range == 255
|
123
|
+
'.+'
|
124
|
+
else
|
125
|
+
RangeRegularExpressionGenerator.new.range_regex(start_range + 1, end_range - 1)
|
126
|
+
end
|
127
|
+
a.push(regex)
|
128
|
+
end.join('\.')
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Proxy::DHCP::Infoblox
|
2
|
+
class PluginConfiguration
|
3
|
+
def load_classes
|
4
|
+
require 'infoblox'
|
5
|
+
require 'dhcp_common/dhcp_common'
|
6
|
+
require 'smart_proxy_dhcp_infoblox/host_ipv4_address_crud'
|
7
|
+
require 'smart_proxy_dhcp_infoblox/fixed_address_crud'
|
8
|
+
require 'smart_proxy_dhcp_infoblox/grid_restart'
|
9
|
+
require 'smart_proxy_dhcp_infoblox/unused_ips'
|
10
|
+
require 'smart_proxy_dhcp_infoblox/dhcp_infoblox_main'
|
11
|
+
end
|
12
|
+
|
13
|
+
def load_dependency_injection_wirings(c, settings)
|
14
|
+
|
15
|
+
|
16
|
+
c.dependency :connection, (lambda do
|
17
|
+
::Infoblox.wapi_version = '2.0'
|
18
|
+
::Infoblox::Connection.new(:username => settings[:username] ,:password => settings[:password],
|
19
|
+
:host => settings[:server], :ssl_opts => {:verify => false})
|
20
|
+
end)
|
21
|
+
|
22
|
+
|
23
|
+
c.dependency :unused_ips, lambda { ::Proxy::DHCP::Infoblox::UnusedIps.new(c.get_dependency(:connection), settings[:use_ranges])}
|
24
|
+
c.dependency :host_ipv4_crud, lambda { ::Proxy::DHCP::Infoblox::HostIpv4AddressCRUD.new(c.get_dependency(:connection))}
|
25
|
+
c.dependency :fixed_address_crud, lambda { ::Proxy::DHCP::Infoblox::FixedAddressCRUD.new(c.get_dependency(:connection))}
|
26
|
+
c.dependency :grid_restart, lambda { ::Proxy::DHCP::Infoblox::GridRestart.new(c.get_dependency(:connection))}
|
27
|
+
c.dependency :dhcp_provider, (lambda do
|
28
|
+
::Proxy::DHCP::Infoblox::Provider.new(
|
29
|
+
c.get_dependency(:connection),
|
30
|
+
settings[:record_type] == 'host' ? c.get_dependency(:host_ipv4_crud) : c.get_dependency(:fixed_address_crud),
|
31
|
+
c.get_dependency(:grid_restart),
|
32
|
+
c.get_dependency(:unused_ips),
|
33
|
+
settings[:subnets])
|
34
|
+
end)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module ::Proxy::DHCP::Infoblox
|
2
|
+
class RecordTypeValidator < ::Proxy::PluginValidators::Base
|
3
|
+
def validate!(settings)
|
4
|
+
return true if ['host', 'fixedaddress'].include?(settings[:record_type])
|
5
|
+
raise ::Proxy::Error::ConfigurationError, "Setting 'record_type' can be set to either 'host' or 'fixedaddress'"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
require 'smart_proxy_dhcp_infoblox/ip_address_arithmetic'
|
3
|
+
|
4
|
+
module Proxy::DHCP::Infoblox
|
5
|
+
class UnusedIps
|
6
|
+
include IpAddressArithmetic
|
7
|
+
attr_reader :connection, :use_ranges
|
8
|
+
|
9
|
+
def initialize(connection, use_ranges)
|
10
|
+
@connection = connection
|
11
|
+
@use_ranges = use_ranges
|
12
|
+
end
|
13
|
+
|
14
|
+
def unused_ip(network_address, from_ip_address, to_ip_address)
|
15
|
+
@use_ranges ? unused_range_ip(network_address, from_ip_address, to_ip_address) : unused_network_ip(network_address, from_ip_address, to_ip_address)
|
16
|
+
end
|
17
|
+
|
18
|
+
def unused_network_ip(network_address, from_ip_address, to_ip_address)
|
19
|
+
find_network(network_address).next_available_ip(1, excluded_ips(find_network(network_address).network, from_ip_address, to_ip_address)).first
|
20
|
+
end
|
21
|
+
|
22
|
+
def unused_range_ip(network_address, from_ip_address, to_ip_address)
|
23
|
+
find_range(network_address, from_ip_address, to_ip_address).next_available_ip(1).first
|
24
|
+
end
|
25
|
+
|
26
|
+
def excluded_ips(subnet_address, from, to)
|
27
|
+
return [] if from.nil? || to.nil?
|
28
|
+
(IPAddr.new(network_cidr_to_range(subnet_address).first)..IPAddr.new(network_cidr_to_range(subnet_address).last)).to_a.map(&:to_s) -
|
29
|
+
(IPAddr.new(from)..IPAddr.new(to)).to_a.map(&:to_s)
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_range(network_address, from, to)
|
33
|
+
ranges = ::Infoblox::Range.find(@connection, 'network~' => network_address)
|
34
|
+
range = (from.nil? || to.nil?) ? ranges.first : ranges.find {|r| r.start_addr == from && r.end_addr == to}
|
35
|
+
raise "No Ranges found for #{network_address} network" if range.nil?
|
36
|
+
range
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_network(network_address)
|
40
|
+
return @memoized_network if !@memoized_network.nil? && @memoized_address == network_address
|
41
|
+
@memoized_address = network_address
|
42
|
+
@memoized_network = ::Infoblox::Network.find(@connection, 'network~' => network_address, '_max_results' => 1).first
|
43
|
+
raise "Subnet #{network_address} not found" if @memoized_network.nil?
|
44
|
+
@memoized_network
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|