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