net-block 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 305d1ead9c8ad555ed1a85b693f2418fbf48f017
4
+ data.tar.gz: 360306d17a457323a61d9cb41b138f9fc52c37f1
5
+ SHA512:
6
+ metadata.gz: d05e3878257f3d67a41033a753f3d320cca18b6a3b148f6dcd16f517d9427613e18bb51f68d0082698c97d614ad5bb1677236171f9e27929d856825c5d392df2
7
+ data.tar.gz: 5485b82cf265d8764cdd624af6855ceb58c9b0884f7b1f08236b445b81c3c58711aaa83fb85ae6a6d2738af0c4875835d616bcb491594977622815a3c4eb9c6c
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
@@ -0,0 +1,27 @@
1
+ ---
2
+ inherit_from: .rubocop_todo.yml
3
+
4
+ Metrics/AbcSize:
5
+ Max: 32
6
+
7
+ Metrics/CyclomaticComplexity:
8
+ Max: 8
9
+
10
+ # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
11
+ # URISchemes: http, https
12
+ Metrics/LineLength:
13
+ Max: 120
14
+
15
+ # Configuration parameters: CountComments.
16
+ Metrics/MethodLength:
17
+ Max: 16
18
+
19
+ Metrics/PerceivedComplexity:
20
+ Max: 8
21
+
22
+ # Configuration parameters: PreferredDelimiters.
23
+ Style/PercentLiteralDelimiters:
24
+ PreferredDelimiters:
25
+ '%i': '[]'
26
+ '%q': '{}'
27
+ '%w': '()'
@@ -0,0 +1,12 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2017-11-06 23:53:58 -0500 using RuboCop version 0.51.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 1
10
+ # Configuration parameters: CountComments.
11
+ Metrics/ClassLength:
12
+ Max: 117
@@ -0,0 +1 @@
1
+ 2.4.1
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in net-block.gemspec
8
+ gemspec
@@ -0,0 +1,43 @@
1
+ Net::Block
2
+ ==========
3
+
4
+ The `net-block` gem provides several foundational classes for managing information about IPv4 networks and addresses, including a BitSet, Address, and Trie.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'net-block'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install net-block
21
+
22
+ ## Usage
23
+
24
+ The included `nb` CLI exposes several commands to summarize a given network specification. Networks are defined in YAML documents with the following layout:
25
+
26
+ ```yml
27
+ root: 192.168.1.0/24
28
+ subnets:
29
+ - address: 192.168.1.0/27
30
+ property1: value1
31
+ - address: 192.168.1.32/27
32
+ property1: value2
33
+ - address: 192.168.1.64/27
34
+ property1: value3
35
+ ```
36
+
37
+ A `root` address in CIDR form that encapsulates all of the given subnets must be provided. The `subnets` property is an array of objects with, at least, an `address` property containing a CIDR string. Additional properties may be provided for metadata.
38
+
39
+ Run `bundle exec nb help` for detailed CLI usage.
40
+
41
+ ## Contributing
42
+
43
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jmanero/net-block.
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ task default: :spec
data/bin/nb ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'net/block/cli'
5
+
6
+ Net::Block::CLI.start
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'block/address'
4
+ require_relative 'block/bit'
5
+ require_relative 'block/bitset'
6
+ require_relative 'block/trie'
7
+
8
+ require_relative 'block/cli'
9
+
10
+ module Net
11
+ # Organize and manipulate IPv4 addresses and network blocks
12
+ #
13
+ module Block
14
+ class << self
15
+ # Build an Address object from a CIDR string
16
+ #
17
+ def address(cidr, **metadata)
18
+ Address.from(cidr, **metadata)
19
+ end
20
+
21
+ # Construct a new Trie structure with the given root network and subnets
22
+ #
23
+ def trie(root, *addresses)
24
+ Trie.from(root, *addresses)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'bitset'
4
+
5
+ module Net
6
+ module Block
7
+ # An IPv4 network address
8
+ #
9
+ class Address
10
+ ZERO = 0
11
+ THIRTY_ONE = 31
12
+ THIRTY_TWO = 32
13
+
14
+ attr_reader :address
15
+ attr_reader :mask
16
+ attr_reader :metadata
17
+
18
+ # Construct an Address from a CIDR string
19
+ #
20
+ def self.from(string, **metadata)
21
+ dotted, mask_length = string.split('/')
22
+ octets = dotted.split('.').map(&:to_i)
23
+
24
+ mask_length ||= THIRTY_TWO # If argument didn't have a /MASK, default to /32
25
+ mask_length = mask_length.to_i
26
+
27
+ ## IPv4 Validation
28
+ unless mask_length.between?(ZERO, THIRTY_TWO)
29
+ raise ArgumentError, "Mask length `#{mask}` is not valid for an IPv4 address"
30
+ end
31
+
32
+ unless octets.length == 4 && octets.all? { |o| o.between?(ZERO, 255) }
33
+ raise ArgumentError, "Address `#{dotted}` is not a valid IPv4 address in dotted-decimal form"
34
+ end
35
+
36
+ ## Generate BitSet from address octets. BitSets are little-endian!
37
+ address = octets.map { |octet| BitSet.from(octet, 8) }.reverse.reduce(:+)
38
+
39
+ new(address, BitSet.mask(mask_length, THIRTY_TWO), **metadata)
40
+ end
41
+
42
+ def initialize(address, mask, **metadata)
43
+ @address = address
44
+ @mask = mask
45
+ @metadata = metadata
46
+ end
47
+
48
+ def network
49
+ Address.new(mask & address, mask.clone)
50
+ end
51
+
52
+ def broadcast
53
+ Address.new(address | !mask, mask.clone)
54
+ end
55
+
56
+ # If this is a network address, ANDing w/ mask should be a NOOP
57
+ #
58
+ def network?
59
+ (address & mask) == address
60
+ end
61
+
62
+ # Test if a given address is a sub-network of this address
63
+ #
64
+ def subnet?(other)
65
+ return false if other.length <= length
66
+
67
+ (mask & other.address) == address
68
+ end
69
+
70
+ # Calculate the super-block of this address. For a host-address, this is
71
+ # the network address. For a network-address, this is the next-shortest mask
72
+ #
73
+ def parent
74
+ return network unless network?
75
+ return @parent unless @parent.nil?
76
+
77
+ ## Calculate the next shortest prefix
78
+ supermask = mask.clone
79
+ supermask.flip!(THIRTY_TWO - length)
80
+
81
+ @parent = Address.new(supermask & address, supermask)
82
+ end
83
+
84
+ # Calculate the bottom subnet of the next most specific prefix
85
+ #
86
+ def left_child
87
+ raise RangeError, 'Cannot allocate an address with mask longer than 32' if length == THIRTY_TWO
88
+ return @left_child unless @left_child.nil?
89
+
90
+ ## Calculate the next longest prefix
91
+ submask = mask.clone
92
+ submask.flip!(THIRTY_ONE - length)
93
+
94
+ @left_child = Address.new(network.address.clone, submask)
95
+ end
96
+
97
+ # Calculate the top subnet of the next most specific prefix
98
+ #
99
+ def right_child
100
+ raise RangeError, 'Cannot allocate an address with mask longer than 32' if length == THIRTY_TWO
101
+ return @right_child unless @right_child.nil?
102
+
103
+ ## Calculate the next longest prefix
104
+ submask = mask.clone
105
+ submask.flip!(THIRTY_ONE - length)
106
+
107
+ ## Increment next-most-significant bit in network address. Should always be Zero
108
+ ## for the `network` Address instance.
109
+ subnet = network.address.clone
110
+ subnet.flip!(THIRTY_ONE - length)
111
+
112
+ @right_child = Address.new(subnet, submask)
113
+ end
114
+
115
+ def length
116
+ mask.ones
117
+ end
118
+
119
+ def ==(other)
120
+ length == other.length && address == other.address
121
+ end
122
+
123
+ def <=>(other)
124
+ address <=> other.address
125
+ end
126
+
127
+ def to_s
128
+ annotations = metadata.map { |k, v| "#{k}: #{v}" }.join(', ')
129
+
130
+ "#{address.v4}/#{mask.ones} #{annotations}"
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Block
5
+ # A boolean value
6
+ #
7
+ class Bit
8
+ PLUS = 1
9
+ ZERO = 0
10
+ MINUS = -1
11
+
12
+ class << self
13
+ # Coerce a value to a Bit
14
+ #
15
+ # NOTE that `to_i` is called on `value`, and may have unexpected effects
16
+ # upon non-numeric values.
17
+ #
18
+ def from(value)
19
+ return value if value.is_a?(self)
20
+ value.to_i.zero? ? Zero : One
21
+ end
22
+
23
+ def ^(other)
24
+ other == self ? Zero : One
25
+ end
26
+
27
+ def one?
28
+ false
29
+ end
30
+
31
+ def zero?
32
+ false
33
+ end
34
+
35
+ def to_s
36
+ to_i.to_s
37
+ end
38
+ end
39
+ end
40
+
41
+ # A One Bit
42
+ #
43
+ class One < Bit
44
+ class << self
45
+ def !
46
+ Zero
47
+ end
48
+
49
+ def &(other)
50
+ other
51
+ end
52
+
53
+ def |(_other)
54
+ One
55
+ end
56
+
57
+ def <=>(other)
58
+ other == self ? Bit::ZERO : Bit::MINUS
59
+ end
60
+
61
+ # Calculate the binary power of the bit at a given position in a BitSet
62
+ #
63
+ def **(other)
64
+ Bit::PLUS << other
65
+ end
66
+
67
+ def one?
68
+ true
69
+ end
70
+
71
+ def to_i
72
+ Bit::PLUS
73
+ end
74
+ end
75
+ end
76
+
77
+ # A Zero Bit
78
+ #
79
+ class Zero < Bit
80
+ class << self
81
+ def !
82
+ One
83
+ end
84
+
85
+ def &(_other)
86
+ Zero
87
+ end
88
+
89
+ def |(other)
90
+ other
91
+ end
92
+
93
+ def <=>(other)
94
+ other == self ? Bit::ZERO : Bit::PLUS
95
+ end
96
+
97
+ def **(_other)
98
+ Bit::ZERO
99
+ end
100
+
101
+ def zero?
102
+ true
103
+ end
104
+
105
+ def to_i
106
+ Bit::ZERO
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative 'bit'
5
+
6
+ module Net
7
+ module Block
8
+ # A vector of Bit objects
9
+ #
10
+ class BitSet
11
+ include Enumerable
12
+ extend Forwardable
13
+
14
+ def_delegators :bits, :[], :length
15
+
16
+ # Populate a BitSet from an Integer value
17
+ #
18
+ def self.from(value, length)
19
+ value = value.to_i
20
+
21
+ result = new
22
+ length.times do
23
+ result.push(Bit.from(value % 2))
24
+ value /= 2 # Ruby Integer division rounds down
25
+ end
26
+
27
+ result
28
+ end
29
+
30
+ # Populate a BitSet for a bit-mask
31
+ #
32
+ def self.mask(ones, length)
33
+ raise RangeError, 'Mask cannot have more ones that the BitSet length' if ones > length
34
+
35
+ result = new(length)
36
+
37
+ until result.ones == ones
38
+ length -= 1
39
+ result.flip!(length)
40
+ end
41
+
42
+ result
43
+ end
44
+
45
+ def initialize(length = 0, fill = Zero)
46
+ @bits = Array.new(length, fill)
47
+ end
48
+
49
+ # Flip a bit at `index` in-place in the BitSet
50
+ #
51
+ def flip!(index)
52
+ bits[index] = !bits[index]
53
+
54
+ self
55
+ end
56
+
57
+ def !
58
+ BitSet.new.tap { |result| each { |bit| result.push(!bit) } }
59
+ end
60
+
61
+ def &(other)
62
+ raise IndexError, 'BitSets must be the same length' unless other.length == length
63
+ BitSet.new.tap { |result| each_with_index { |bit, i| result.push(bit & other[i]) } }
64
+ end
65
+
66
+ def |(other)
67
+ raise IndexError, 'BitSets must be the same length' unless other.length == length
68
+ BitSet.new.tap { |result| each_with_index { |bit, i| result.push(bit | other[i]) } }
69
+ end
70
+
71
+ def ==(other)
72
+ other.length == length && other.bits == bits
73
+ end
74
+
75
+ def <=>(other)
76
+ raise IndexError, 'BitSets must be the same length' unless other.length == length
77
+
78
+ ## Comparison searches from MSB (31 for an IPV4) to LSB for the first
79
+ ## pair of bits that do not match, and returns that comparison
80
+ index = length - 1
81
+ index -= 1 until bits[index] != other.bits[index] || index.zero?
82
+
83
+ other.bits[index] <=> bits[index]
84
+ end
85
+
86
+ # Join two BitSets into a new BitSet
87
+ def +(other)
88
+ BitSet.new.tap { |result| result.bits = bits + other.bits }
89
+ end
90
+
91
+ def clone
92
+ BitSet.new.tap { |result| result.bits = bits.clone }
93
+ end
94
+
95
+ # Construct a BitSet from a sub-section of this BitSet. `from` is inclusive,
96
+ # `to` is exclusive.
97
+ #
98
+ def slice(from, to)
99
+ raise ArgumentError, 'Argument `from` must be less than or equal to `to`' unless from <= to
100
+ raise ArgumentError, 'Argument `from` must be between 0 and the set length' unless from.between?(0, length)
101
+ raise ArgumentError, 'Argument `to` must be between 0 and the set length' unless from.between?(0, length)
102
+
103
+ BitSet.new(to - from).tap { |set| set.bits = bits.slice(from, to - from) }
104
+ end
105
+
106
+ def ones
107
+ count(&:one?)
108
+ end
109
+
110
+ def zeros
111
+ count(&:zero?)
112
+ end
113
+
114
+ # Calculate the Integer value of the BitSet
115
+ def to_i
116
+ reduce([0, 0]) { |(value, index), bit| [value + bit**index, index + 1] }.first
117
+ end
118
+
119
+ def to_s
120
+ "<#{self.class.name}(#{length}): #{reverse.join(' ')}>"
121
+ end
122
+
123
+ # Helper to format the BitSet as a dotted-quad IPv4 address string
124
+ #
125
+ def v4
126
+ raise RangeError, 'BitSet must have a length of 32 to format as an IPv4 address!' unless length == 32
127
+ [slice(24, 32), slice(16, 24), slice(8, 16), slice(0, 8)].map(&:to_i).join('.')
128
+ end
129
+
130
+ protected
131
+
132
+ # Allow a BitSet to modify the underlying bit-array of another BitSet
133
+ #
134
+ attr_reader :bits
135
+ attr_writer :bits
136
+
137
+ def_delegators :bits, :each, :push, :reverse, :unshift
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'yaml'
5
+
6
+ require_relative '../block'
7
+
8
+ module Net
9
+ module Block
10
+ # CLI for nb executable
11
+ #
12
+ class CLI < Thor
13
+ desc 'sumarize NETWORK', 'Read a network allocation table from a YAML file and sumarize'
14
+ def summarize(path)
15
+ trie = Utils.load_trie(path)
16
+
17
+ ## Aggregations
18
+ aggregates = trie.aggregate
19
+ holes = trie.holes
20
+
21
+ say "Found #{aggregates.length} continuous netblocks:"
22
+ aggregates.each { |block| puts " - #{block.network}" }
23
+ puts '---'
24
+ puts ''
25
+
26
+ say "Found #{holes.length} unallocated netblocks:"
27
+ holes.sort_by(&:length).each { |address| puts " - #{address}" }
28
+ end
29
+
30
+ desc 'aggregate NETWORK', 'Read a network allocation table from a\
31
+ YAML file aggregate into the largest possible network blocks'
32
+
33
+ def aggregate(path)
34
+ aggregates = Utils.load_trie(path).aggregate
35
+
36
+ say "Found #{aggregates.length} continuous netblocks:"
37
+ aggregates.each { |block| puts "---\n#{block}" }
38
+ end
39
+
40
+ desc 'next NETWORK MASK', 'Find the next available address of the given\
41
+ MASK length in the network allocation table'
42
+ def next(path, length)
43
+ suitable = Utils.load_trie(path).next(length.to_i)
44
+
45
+ say "Found #{suitable.length} possible addresses:"
46
+ suitable.each { |address| puts " - #{address}" }
47
+ end
48
+ end
49
+
50
+ # Common CLI helper methods
51
+ #
52
+ module Utils
53
+ class << self
54
+ def load_yaml(path)
55
+ content = IO.read(File.expand_path(path))
56
+ YAML.safe_load(content)
57
+ end
58
+
59
+ def load_trie(path)
60
+ network = load_yaml(path)
61
+
62
+ ## Parse subnet entries {address: 'CIDR/NN', ...metadata: tags}
63
+ addresses = network.fetch('subnets').map do |entry|
64
+ cidr = entry.delete('address')
65
+ metadata = entry.each_with_object({}) { |(k, v), object| object[k.to_sym] = v }
66
+
67
+ Address.from(cidr, **metadata)
68
+ end
69
+
70
+ ## Build the Trie
71
+ Trie.from(network.fetch('root'), addresses)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,211 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ module Net
5
+ module Block
6
+ # Organize IPv4 addresses into a binary tree
7
+ #
8
+ class Trie
9
+ attr_reader :parent
10
+ attr_reader :network
11
+ attr_reader :left
12
+ attr_reader :right
13
+
14
+ def self.root
15
+ new(nil, Address.from('0.0.0.0/0'))
16
+ end
17
+
18
+ def self.from(root, *addresses)
19
+ new(nil, Address.from(root)).tap do |trie|
20
+ addresses.flatten.each { |addr| trie.insert(addr) }
21
+ end
22
+ end
23
+
24
+ def initialize(parent, network)
25
+ @parent = parent
26
+ @network = network
27
+ end
28
+
29
+ # Check if the node has a left-hand child, and if the child satasifes an
30
+ # optional block condition
31
+ #
32
+ def left?
33
+ if @left.nil? then false
34
+ elsif block_given? then yield left
35
+ else true
36
+ end
37
+ end
38
+
39
+ # Check if the node has a right-hand child, and if the child satasifes an
40
+ # optional block condition
41
+ #
42
+ def right?
43
+ if @right.nil? then false
44
+ elsif block_given? then yield right
45
+ else true
46
+ end
47
+ end
48
+
49
+ # Check if the node has any children that satasfy an optional block condition
50
+ #
51
+ def any?(&block)
52
+ left?(&block) || right?(&block)
53
+ end
54
+
55
+ # Check if the node has both children that satasfy an optional block condition
56
+ #
57
+ def all?(&block)
58
+ left?(&block) && right?(&block)
59
+ end
60
+
61
+ # Check if the node has exactly one child that satasifes an optional block
62
+ # condition
63
+ #
64
+ def one?(&block)
65
+ left?(&block) ^ right?(&block)
66
+ end
67
+
68
+ # Check if the node has no children
69
+ #
70
+ def empty?
71
+ !any?
72
+ end
73
+
74
+ # Check if the node or any of its decendants have only one child
75
+ #
76
+ def sparse?
77
+ return true if one?
78
+ return false if empty?
79
+
80
+ ## all? -> true
81
+ left.sparse? || right.sparse?
82
+ end
83
+
84
+ # Evaluate a block for each of the node's decendants. The node itself is evaluated first,
85
+ # followed by left and left's decendants, then right and right's decendants. Blocks can
86
+ # raise `StopIteration` to stop traversing farther into the Trie
87
+ #
88
+ def each(&block)
89
+ begin
90
+ yield self
91
+ rescue StopIteration
92
+ return ## break
93
+ end
94
+
95
+ left.each(&block) if left?
96
+ right.each(&block) if right?
97
+
98
+ self
99
+ end
100
+
101
+ # Allow reduction operations to traverse the Trie. Blocks can raise
102
+ # `StopIteration` to stop traversing farther into the Trie
103
+ #
104
+ # @yields collection, node
105
+ #
106
+ def collect(collection = [], &block)
107
+ begin
108
+ yield collection, self
109
+ rescue StopIteration
110
+ return ## break
111
+ end
112
+
113
+ left.collect(collection, &block) if left?
114
+ right.collect(collection, &block) if right?
115
+
116
+ collection
117
+ end
118
+
119
+ # Return the address of this node's unallocated child if only one child
120
+ # is populated
121
+ #
122
+ def unallocated
123
+ return network.left_child unless left?
124
+ return network.right_child unless right?
125
+ end
126
+
127
+ # Hash an Address object into the correct Trie node
128
+ #
129
+ def insert(address)
130
+ unless network.subnet?(address)
131
+ raise ArgumentError, 'Cannot insert an address that is not a subnet of this node'
132
+ end
133
+
134
+ ## Already allocated. This is somewhat undefined.
135
+ return if network == address
136
+
137
+ ## Direct descendant. Don't let a new entry clobber an existing node.
138
+ return @left ||= Trie.new(self, address) if network.left_child == address
139
+ return @right ||= Trie.new(self, address) if network.right_child == address
140
+
141
+ if network.left_child.subnet?(address)
142
+ @left ||= Trie.new(self, network.left_child)
143
+ left.insert(address)
144
+ elsif network.right_child.subnet?(address)
145
+ @right ||= Trie.new(self, network.right_child)
146
+ right.insert(address)
147
+ end
148
+ end
149
+
150
+ # Find the least-specific prefix for continuous blocks of hashed IPv4 addresses
151
+ #
152
+ def aggregate
153
+ ## Find nodes in the trie whose offspring from complete sets. e.g. every
154
+ ## node that has two children, or zero children, recursively. Nodes with
155
+ ## one child can not be aggregated without covering a hole
156
+ collect do |aggregates, node|
157
+ next if node.sparse?
158
+
159
+ aggregates << node
160
+ raise StopIteration
161
+ end
162
+ end
163
+
164
+ # Find unallocated prefixes in a set of IPv4 addresses
165
+ #
166
+ def holes
167
+ collect do |unallocated, node|
168
+ ## This children and all of this node are non-sparse
169
+ raise StopIteration unless node.sparse?
170
+
171
+ ## If this node is missing a child, collect that address. In any case,
172
+ ## keep searching this node's decendants for unallocated children
173
+ unallocated << node.unallocated unless node.all?
174
+ end
175
+ end
176
+
177
+ # Find possible addresses for a given mask length
178
+ #
179
+ def next(length)
180
+ ## Select blocks with short ehough masks
181
+ suitable = holes.select { |address| address.length <= length }.sort
182
+
183
+ ## Find the first subnet of each available block that satasifes the given length
184
+ suitable.map do |address|
185
+ address = address.left_child until address.length == length
186
+ address
187
+ end
188
+ end
189
+
190
+ # Flatten the Trie into an array of its hashed network Address instances
191
+ #
192
+ def flatten
193
+ return [network] if empty?
194
+
195
+ children = []
196
+ children += left.flatten if left?
197
+ children += right.flatten if right?
198
+
199
+ children
200
+ end
201
+
202
+ def to_s(indent = 0)
203
+ lines = [(' ' * indent) + network.to_s]
204
+ lines << left.to_s(indent + 2) if left?
205
+ lines << right.to_s(indent + 2) if right?
206
+
207
+ lines.join("\n")
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ module Block
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'net/block/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'net-block'
9
+ spec.version = Net::Block::VERSION
10
+ spec.authors = ['John Manero']
11
+ spec.email = ['john.manero@gmail.com']
12
+
13
+ spec.summary = 'Organize sets of IPv4 addresses into hierarchial structures for processing'
14
+ spec.description = 'Organize sets of IPv4 addresses into hierarchial structures for processing'
15
+ spec.homepage = 'https://github.com/jmanero/netc'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.15'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+
27
+ spec.add_dependency 'thor', '~> 0.20'
28
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: net-block
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - John Manero
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-11-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.20'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.20'
55
+ description: Organize sets of IPv4 addresses into hierarchial structures for processing
56
+ email:
57
+ - john.manero@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rubocop.yml"
64
+ - ".rubocop_todo.yml"
65
+ - ".ruby-version"
66
+ - Gemfile
67
+ - README.md
68
+ - Rakefile
69
+ - bin/nb
70
+ - lib/net/block.rb
71
+ - lib/net/block/address.rb
72
+ - lib/net/block/bit.rb
73
+ - lib/net/block/bitset.rb
74
+ - lib/net/block/cli.rb
75
+ - lib/net/block/trie.rb
76
+ - lib/net/block/version.rb
77
+ - net-block.gemspec
78
+ homepage: https://github.com/jmanero/netc
79
+ licenses: []
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 2.6.11
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Organize sets of IPv4 addresses into hierarchial structures for processing
101
+ test_files: []