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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +27 -0
- data/.rubocop_todo.yml +12 -0
- data/.ruby-version +1 -0
- data/Gemfile +8 -0
- data/README.md +43 -0
- data/Rakefile +4 -0
- data/bin/nb +6 -0
- data/lib/net/block.rb +28 -0
- data/lib/net/block/address.rb +134 -0
- data/lib/net/block/bit.rb +111 -0
- data/lib/net/block/bitset.rb +140 -0
- data/lib/net/block/cli.rb +76 -0
- data/lib/net/block/trie.rb +211 -0
- data/lib/net/block/version.rb +7 -0
- data/net-block.gemspec +28 -0
- metadata +101 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -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': '()'
|
data/.rubocop_todo.yml
ADDED
@@ -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
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.1
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/bin/nb
ADDED
data/lib/net/block.rb
ADDED
@@ -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
|
data/net-block.gemspec
ADDED
@@ -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: []
|