net-block 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []