From eb017ad756f4ba8db98d20dbb777baddb6836d01 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Juho=20M=C3=A4kinen?= <juho.makinen@applifier.com>
Date: Fri, 4 Nov 2016 13:28:07 +0200
Subject: [PATCH] Add allocate() method

The allocate() method allows to use an IPAddress::IPv4 or ::IPv6 object
to allocate individual host addresses so that the IPAddress object
tracks the state of which address was previously allocated.

Example:

  ip = IPAddress("10.0.0.0/24")
  ip.allocate
    #=> "10.0.0.1/24"
  ip.allocate
    #=> "10.0.0.2/24"

There is support for both IPv4 and IPv6 with full test coverage.
---
 lib/ipaddress/ipv4.rb       | 31 +++++++++++++++++++++++++++++++
 lib/ipaddress/ipv6.rb       | 31 +++++++++++++++++++++++++++++++
 test/ipaddress/ipv4_test.rb | 24 ++++++++++++++++++++++++
 test/ipaddress/ipv6_test.rb | 24 ++++++++++++++++++++++++
 4 files changed, 110 insertions(+)

diff --git a/lib/ipaddress/ipv4.rb b/lib/ipaddress/ipv4.rb
index a123e71..080f3d6 100644
--- a/lib/ipaddress/ipv4.rb
+++ b/lib/ipaddress/ipv4.rb
@@ -89,6 +89,7 @@ module IPAddress;
       # 32 bits interger containing the address
       @u32 = (@octets[0]<< 24) + (@octets[1]<< 16) + (@octets[2]<< 8) + (@octets[3])
       
+      @allocator = 0
     end # def initialize
 
     #
@@ -1063,6 +1064,36 @@ module IPAddress;
       self.new "#{address}/#{prefix}"
     end
 
+    #
+    # Allocates a new ip from the current subnet. Optional skip parameter
+    # can be used to skip addresses.
+    #
+    # Will raise StopIteration exception when all addresses have been allocated
+    #
+    # Example:
+    #
+    #    ip = IPAddress("10.0.0.0/24")
+    #    ip.allocate
+    #      #=> "10.0.0.1/24"
+    #    ip.allocate
+    #      #=> "10.0.0.2/24"
+    #    ip.allocate(2)
+    #      #=> "10.0.0.5/24"
+    #
+    #
+    # Uses an internal @allocator which tracks the state of allocated
+    # addresses.
+    #
+    def allocate(skip=0)
+        @allocator += 1 + skip
+
+        next_ip = network_u32+@allocator
+        if next_ip > broadcast_u32+1
+            raise StopIteration
+        end
+        self.class.parse_u32(network_u32+@allocator, @prefix)
+    end
+
     #
     # private methods
     #
diff --git a/lib/ipaddress/ipv6.rb b/lib/ipaddress/ipv6.rb
index 6ae651d..284308c 100644
--- a/lib/ipaddress/ipv6.rb
+++ b/lib/ipaddress/ipv6.rb
@@ -102,6 +102,7 @@ module IPAddress;
       end
 
       @prefix = Prefix128.new(netmask ? netmask : 128)
+      @allocator = 0
 
     end # def initialize
 
@@ -634,6 +635,36 @@ module IPAddress;
     def self.parse_hex(hex, prefix=128)
       self.parse_u128(hex.hex, prefix)
     end
+
+    #
+    # Allocates a new ip from the current subnet. Optional skip parameter
+    # can be used to skip addresses.
+    #
+    # Will raise StopIteration exception when all addresses have been allocated
+    #
+    # Example:
+    #
+    #    ip = IPAddress("10.0.0.0/24")
+    #    ip.allocate
+    #      #=> "10.0.0.1/24"
+    #    ip.allocate
+    #      #=> "10.0.0.2/24"
+    #    ip.allocate(2)
+    #      #=> "10.0.0.5/24"
+    #
+    #
+    # Uses an internal @allocator which tracks the state of allocated
+    # addresses.
+    #
+    def allocate(skip=0)
+        @allocator += 1 + skip
+
+        next_ip = network_u128+@allocator
+        if next_ip > broadcast_u128
+            raise StopIteration
+        end
+        self.class.parse_u128(next_ip, @prefix)
+    end
     
     private
 
diff --git a/test/ipaddress/ipv4_test.rb b/test/ipaddress/ipv4_test.rb
index 19264e2..df60bb1 100644
--- a/test/ipaddress/ipv4_test.rb
+++ b/test/ipaddress/ipv4_test.rb
@@ -595,6 +595,30 @@ class IPv4Test < Minitest::Test
     assert_equal "192.168.200.0/24", ip.to_string
   end
 
+  def test_allocate_addresses
+    ip = @klass.new("10.0.0.0/24")
+    ip1 = ip.allocate
+    ip2 = ip.allocate
+    ip3 = ip.allocate
+    assert_equal "10.0.0.1/24", ip1.to_string
+    assert_equal "10.0.0.2/24", ip2.to_string
+    assert_equal "10.0.0.3/24", ip3.to_string
+  end
+
+  def test_allocate_can_skip_addresses
+    ip = @klass.new("10.0.0.0/24")
+    ip1 = ip.allocate(2)
+    assert_equal "10.0.0.3/24", ip1.to_string
+  end
+
+  def test_allocate_will_raise_stopiteration
+    ip = @klass.new("10.0.0.0/30")
+    ip.allocate(3)
+    assert_raises (StopIteration) do
+      ip.allocate
+    end
+  end
+
 end # class IPv4Test
 
   
diff --git a/test/ipaddress/ipv6_test.rb b/test/ipaddress/ipv6_test.rb
index 7eaa765..3990129 100644
--- a/test/ipaddress/ipv6_test.rb
+++ b/test/ipaddress/ipv6_test.rb
@@ -234,6 +234,30 @@ class IPv6Test < Minitest::Test
     assert_equal expected, arr
   end
 
+  def test_allocate_addresses
+    ip = @klass.new("2001:db8::4/125")
+    ip1 = ip.allocate
+    ip2 = ip.allocate
+    ip3 = ip.allocate
+    assert_equal "2001:db8::1", ip1.compressed
+    assert_equal "2001:db8::2", ip2.compressed
+    assert_equal "2001:db8::3", ip3.compressed
+  end
+
+  def test_allocate_can_skip_addresses
+    ip = @klass.new("2001:db8::4/125")
+    ip1 = ip.allocate(2)
+    assert_equal "2001:db8::3", ip1.compressed
+  end
+
+  def test_allocate_will_raise_stopiteration
+    ip = @klass.new("2001:db8::4/125")
+    ip.allocate(6)
+    assert_raises (StopIteration) do
+      ip.allocate
+    end
+  end
+
   def test_method_compare
     ip1 = @klass.new("2001:db8:1::1/64")
     ip2 = @klass.new("2001:db8:2::1/64")
-- 
GitLab