diff --git a/Makefile.am b/Makefile.am index 90cc61b2549294c335c8651a8a61351d143878b9..91ab8a47d1994bc68264f2cb51de5d3cb2cc7975 100644 --- a/Makefile.am +++ b/Makefile.am @@ -160,6 +160,7 @@ pkgpython_PYTHON = \ lib/ssh.py \ lib/storage.py \ lib/uidpool.py \ + lib/ippool.py \ lib/workerpool.py client_PYTHON = \ diff --git a/lib/ippool.py b/lib/ippool.py new file mode 100644 index 0000000000000000000000000000000000000000..d841e5680708d80cf0afd6e99456bead16655e60 --- /dev/null +++ b/lib/ippool.py @@ -0,0 +1,220 @@ +# +# + +# Copyright (C) 2010 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +"""Ip address pool management functions. + +""" + +import base64 +import ipaddr + +from ganeti import errors + +from bitarray import bitarray + + +class IPv4PoolError(Exception): + """ Generic IPv4 pool error + + """ + + +class IPv4PoolFull(IPv4PoolError): + """ IPv4 pool-is-full error + + """ + + +class IPv4Network(object): + def __init__(self, net, gateway=None, pool=None): + """ Initialize a new IPv4 address pool + + """ + self.net = ipaddr.IPv4Network(net) + self.gateway = None + self.size = 2**(32 - self.net.prefixlen) + if self.size <= 2: + raise IPv4PoolError("Subnet too small") + + if pool is None: + self._pool = bitarray(self.size) + self._pool.setall(False) + self._pool[0] = True + self._pool[-1] = True + else: + self._pool = bitarray() + self._pool.fromstring(pool) + + if gateway is not None: + self.gateway = ipaddr.IPv4Address(gateway) + + if not self.net.Contains(self.gateway): + raise IPv4PoolError("Gateway must lie in the subnet") + + if pool is None: + try: + self.reserve(gateway) + except IPv4PoolError: + raise IPv4PoolError("Gateway cannot be network or broadcast") + + def __repr__(self): + return "<IPv4Network: %s, gateway: %s, free: %d>" % (self.net, self.gateway, + self.free_count) + + def todict(self): + """Convert an IPv4Network to a dictionary + + """ + d = {} + d["net"] = str(self.net) + if self.gateway is not None: + d["gateway"] = str(self.gateway) + + # JSON can't handle binary data, so encode the pool using base64 + d["pool"] = base64.encodestring(self._pool.tostring()).strip() + return d + + @classmethod + def fromdict(cls, d): + """Create an IPv4Network instance from a dictionary + + @type d: dict + @param d: dictionary containing the pool data + @rtype: IPv4Network + @return: IPv4Network object for the specified pool + + """ + pool = None + gateway = None + if "pool" in d: + pool = base64.decodestring(d["pool"]) + if "gateway" in d: + gateway = d["gateway"] + + return cls(d["net"], gateway, pool) + + @property + def full(self): + """Check whether the pool is full + + """ + return self._pool.all() + + @property + def reserved_count(self): + """Get the count of reserved IPs + + """ + return self._pool.count(True) + + @property + def free_count(self): + """Get the count of free IPs + + """ + return self._pool.count(False) + + @property + def map(self): + """Return an intuitive textual representation of the pool status. + + """ + ipmap = "" + for ip in self._pool: + if ip: + ipmap += "X" + else: + ipmap += "." + return ipmap + + def _ip_index(self, address): + addr = ipaddr.IPv4Address(address) + + if not self.net.Contains(addr): + raise IPv4PoolError("%s does not contain %s" % (self.net, address)) + + idx = int(addr) - int(self.net.network) + return idx + + def _mark(self, address, value=True): + idx = self._ip_index(address) + self._pool[idx] = value + + def is_reserved(self, address): + """Check whether an address is reserved + + """ + idx = self._ip_index(address) + return self._pool[idx] + + def reserve(self, address): + """Mark an address as used. + + Fail if the address is already marked as used + """ + if self.is_reserved(address): + raise IPv4PoolError("%s already reserved" % address) + self._mark(address) + + def release(self, address): + """Mark an address as free + + """ + self._mark(address, False) + + def get_free_address(self): + """Return the first free address in the pool + + """ + if self.full: + raise IPv4PoolFull("Pool is full") + idx = self._pool.index(False) + addr = str(self.net[idx]) + self.reserve(addr) + return addr + + def __iter_free(self): + for idx in self._pool.search("0", 64): + yield str(self.net[idx]) + + def generate_free(self): + return self.__iter_free().next + + +def ParseIPv4Pool(value): + """Parse an IP address pool definition + + @type value: str + @param value: string representation of the user-id pool. + The accepted input format is the following: + <CIDR> <gateway> + Example: 10.0.1.0/24 10.0.1.1 + + @rtype: tuple + @return: A tuple (start, end, prefix, gateway) + + """ + components = value.split() + if len(components) != 2: + raise errors.OpPrereqError("Invalid IP pool definition", errors.ECODE_INVAL) + + return IPv4Network(*components) +