Source code for lago.subnet_lease

#
# Copyright 2014-2017 Red Hat, 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
#
# Refer to the README and COPYING files for full details of the license
#

from future.utils import raise_from
from future.builtins import super
import functools
import json
import os
import logging
from netaddr import IPNetwork, AddrFormatError
from textwrap import dedent
from lockfile.mkdirlockfile import LockFailed

from .config import config
from . import utils, log_utils

LOGGER = logging.getLogger(__name__)
LogTask = functools.partial(log_utils.LogTask, logger=LOGGER)
LOCK_NAME = 'leases'


[docs]class SubnetStore(object): """ SubnetStore object represents a store of subnets used by lago for network bridges. .. note:: Currently only /24 ranges are handled, and all of them under the 192.168._min_third_octet to 192.168._max_third_octet ranges. The leases are stored under the store's directory (which is specified with the `path` argument) as json files with the form:: [ "/path/to/prefix/uuid/file", "uuid_hash", ] Where the `uuid_hash` is the 32 char uuid of the prefix (the contents of the uuid file at the time of doing the lease). The helper class :class:`Lease` is used to abstract the interaction with the lease files in the store (each file will be represented with a Lease object). Cleanup of stale leases is done in a lazy manner during a request for a lease. The store will remove at most 1 stale lease in each request (see SubnetStore._lease_valid for more info). Attributes: _path (str): Path to the store, if not specified defaults to the value of `lease_dir` in the config _cidr (int): Number of bits dedicated for the network address. Has a fixed value of 24. _subnet_template (str): A template for creating ip address. Has a fixed value of `192.168.{}.0` _min_third_octet (int): The minimum value of the subnets' last octet. _max_third_octet (int): The maximum value of the subnets' last octet. _min_subnet (netaddr.IPNetwork): The lowest subnet in the range of the store. _max_subnet (netaddr.IPNetwork): The highest subnet in the range of the store. """ def __init__( self, path=None, min_third_octet=200, max_third_octet=255, ): self._path = path or config['lease_dir'] self._cidr = 24 self._subnet_template = '{}/{}'.format('192.168.{}.0', self._cidr) self._min_third_octet = min_third_octet self._max_third_octet = max_third_octet self._min_subnet = IPNetwork( self._subnet_template.format(min_third_octet) ) self._max_subnet = IPNetwork( self._subnet_template.format(max_third_octet) ) self._validate_lease_dir()
[docs] def _create_lock(self): return utils.LockFile( path=os.path.join(self.path, LOCK_NAME), timeout=5 )
[docs] def _validate_lease_dir(self): """ Validate that the directory used by this store exist, otherwise create it. """ try: if not os.path.isdir(self.path): os.makedirs(self.path) except OSError as e: raise_from( LagoSubnetLeaseBadPermissionsException(self.path, e.strerror), e )
[docs] def acquire(self, uuid_path, subnet=None): """ Lease a free subnet for the given uuid path. If subnet is given, try to lease that subnet, otherwise try to lease a free subnet. Args: uuid_path (str): Path to the uuid file of a :class:`lago.Prefix` subnet (str): A subnet to lease. Returns: netaddr.IPAddress: An object which represents the subnet. Raises: LagoSubnetLeaseException: 1. If this store is full 2. If the requested subnet is already taken. LagoSubnetLeaseLockException: If the lock to self.path can't be acquired. """ try: with self._create_lock(): if subnet: LOGGER.debug('Trying to acquire subnet {}'.format(subnet)) acquired_subnet = self._acquire_given_subnet( uuid_path, subnet ) else: LOGGER.debug('Trying to acquire a free subnet') acquired_subnet = self._acquire(uuid_path) return acquired_subnet except (utils.TimerException, LockFailed): raise LagoSubnetLeaseLockException(self.path)
[docs] def _acquire(self, uuid_path): """ Lease a free network for the given uuid path Args: uuid_path (str): Path to the uuid file of a :class:`lago.Prefix` Returns: netaddr.IPNetwork: Which represents the selected subnet Raises: LagoSubnetLeaseException: If the store is full """ for index in range(self._min_third_octet, self._max_third_octet + 1): lease = self.create_lease_object_from_idx(index) if self._lease_valid(lease): continue self._take_lease(lease, uuid_path, safe=False) return lease.to_ip_network() raise LagoSubnetLeaseStoreFullException(self.get_allowed_range())
[docs] def _acquire_given_subnet(self, uuid_path, subnet): """ Try to create a lease for subnet Args: uuid_path (str): Path to the uuid file of a :class:`lago.Prefix` subnet (str): dotted ipv4 subnet (for example ```192.168.200.0```) Returns: netaddr.IPNetwork: Which represents the selected subnet Raises: LagoSubnetLeaseException: If the requested subnet is not in the range of this store or its already been taken """ lease = self.create_lease_object_from_subnet(subnet) self._take_lease(lease, uuid_path) return lease.to_ip_network()
[docs] def _lease_valid(self, lease): """ Check if the given lease exist and still has a prefix that owns it. If the lease exist but its prefix isn't, remove the lease from this store. Args: lease (lago.subnet_lease.Lease): Object representation of the lease Returns: str or None: If the lease and its prefix exists, return the path to the uuid of the prefix, else return None. """ if not lease.exist: return None if lease.has_env: return lease.uuid_path else: self._release(lease) return None
[docs] def _take_lease(self, lease, uuid_path, safe=True): """ Persist the given lease to the store and make the prefix in uuid_path his owner Args: lease(lago.subnet_lease.Lease): Object representation of the lease uuid_path (str): Path to the prefix uuid safe (bool): If true (the default), validate the the lease isn't taken. Raises: LagoSubnetLeaseException: If safe == True and the lease is already taken. """ if safe: lease_taken_by = self._lease_valid(lease) if lease_taken_by and lease_taken_by != uuid_path: raise LagoSubnetLeaseTakenException( lease.subnet, lease_taken_by ) with open(uuid_path) as f: uuid = f.read() with open(lease.path, 'wt') as f: utils.json_dump((uuid_path, uuid), f) LOGGER.debug( 'Assigned subnet lease {} to {}'.format(lease.path, uuid_path) )
[docs] def list_leases(self, uuid=None): """ List current subnet leases Args: uuid(str): Filter the leases by uuid Returns: list of :class:~Lease: current leases """ try: lease_files = os.listdir(self.path) except OSError as e: raise_from( LagoSubnetLeaseBadPermissionsException(self.path, e.strerror), e ) leases = [ self.create_lease_object_from_idx(lease_file.split('.')[0]) for lease_file in lease_files ] if not uuid: return leases else: return [lease for lease in leases if lease.uuid == uuid]
[docs] def release(self, subnets): """ Free the lease of the given subnets Args: subnets (list of str or netaddr.IPAddress): dotted ipv4 subnet in CIDR notation (for example ```192.168.200.0/24```) or IPAddress object. Raises: LagoSubnetLeaseException: If subnet is a str and can't be parsed LagoSubnetLeaseLockException: If the lock to self.path can't be acquired. """ if isinstance(subnets, str) or isinstance(subnets, IPNetwork): subnets = [subnets] subnets_iter = ( str(subnet) if isinstance(subnet, IPNetwork) else subnet for subnet in subnets ) try: with self._create_lock(): for subnet in subnets_iter: self._release(self.create_lease_object_from_subnet(subnet)) except (utils.TimerException, LockFailed): raise LagoSubnetLeaseLockException(self.path)
[docs] def _release(self, lease): """ Free the given lease Args: lease (lago.subnet_lease.Lease): The lease to free """ if lease.exist: os.unlink(lease.path) LOGGER.debug('Removed subnet lease {}'.format(lease.path))
[docs] def _lease_owned(self, lease, current_uuid_path): """ Checks if the given lease is owned by the prefix whose uuid is in the given path Note: The prefix must be also in the same path it was when it took the lease Args: path (str): Path to the lease current_uuid_path (str): Path to the uuid to check ownership of Returns: bool: ``True`` if the given lease in owned by the prefix, ``False`` otherwise """ prev_uuid_path, prev_uuid = lease.metadata with open(current_uuid_path) as f: current_uuid = f.read() return \ current_uuid_path == prev_uuid_path and \ prev_uuid == current_uuid
[docs] def create_lease_object_from_idx(self, idx): """ Create a lease from self._subnet_template and put idx as its third octet. Args: idx (str): The value of the third octet Returns: Lease: Lease object which represents the requested subnet. Raises: LagoSubnetLeaseOutOfRangeException: If the resultant subnet is malformed or out of the range of the store. """ return self.create_lease_object_from_subnet( self._subnet_template.format(idx) )
[docs] def create_lease_object_from_subnet(self, subnet): """ Create a lease from ip in a dotted decimal format, (for example `192.168.200.0/24`). the _cidr will be added if not exist in `subnet`. Args: subnet (str): The value of the third octet Returns: Lease: Lease object which represents the requested subnet. Raises: LagoSubnetLeaseOutOfRangeException: If the resultant subnet is malformed or out of the range of the store. """ if '/' not in subnet: subnet = '{}/{}'.format(subnet, self._cidr) try: if not self.is_leasable_subnet(subnet): raise LagoSubnetLeaseOutOfRangeException( subnet, self.get_allowed_range() ) except AddrFormatError: raise LagoSubnetLeaseMalformedAddrException(subnet) return Lease(store_path=self.path, subnet=subnet)
[docs] def is_leasable_subnet(self, subnet): """ Checks if a given subnet is inside the defined provision-able range Args: subnet (str): Ip in dotted decimal format with _cidr notation (for example `192.168.200.0/24`) Returns: bool: True if subnet can be parsed into IPNetwork object and is inside the range, False otherwise Raises: netaddr.AddrFormatError: If subnet can not be parsed into an ip. """ return \ self._min_subnet <= \ IPNetwork(subnet) <= \ self._max_subnet
[docs] def get_allowed_range(self): """ Returns: str: The range of the store (with lowest and highest subnets as the bounds). """ return '{} - {}'.format(self._min_subnet, self._max_subnet)
@property def path(self): return self._path
[docs]class Lease(object): """ Lease object is an abstraction of a lease file. Attributes: _store_path (str): Path to the lease's store. _subnet (str): The subnet that this lease represents _path (str): The path to the lease file """ def __init__(self, store_path, subnet): self._store_path = store_path self._subnet = subnet self._path = None self._realise_lease_path()
[docs] def _realise_lease_path(self): ip = self.subnet.split('/')[0] idx = ip.split('.')[2] self._path = os.path.join(self._store_path, '{}.lease'.format(idx))
[docs] def to_ip_network(self): return IPNetwork(self.subnet)
@property def valid(self): if self.exist: return self.has_env else: return False @property def metadata(self): with open(self.path) as f: uuid_path, uuid = json.load(f) return uuid_path, uuid @property def uuid(self): return self.metadata[1] @property def uuid_path(self): return self.metadata[0] @property def has_env(self): return self._has_env()
[docs] def _has_env(self, uuid_path=None, uuid=None): if not (uuid_path and uuid): uuid_path, uuid = self.metadata if not os.path.isfile(uuid_path): return False with open(uuid_path, mode='rt') as f: if f.read() == uuid: return True else: return False
@property def exist(self): return os.path.isfile(self.path) @property def path(self): return self._path @path.setter def path(self, data): self._path = data @property def subnet(self): return self._subnet @subnet.setter def subnet(self, data): self._subnet = data def __str__(self): return self.subnet
[docs]class LagoSubnetLeaseException(utils.LagoException): def __init__(self, msg, prv_msg=None): if prv_msg is not None: msg = msg + '\nOriginal Exception: {0}'.format(prv_msg) super().__init__(msg)
[docs]class LagoSubnetLeaseLockException(LagoSubnetLeaseException): def __init__(self, store_path): super().__init__( dedent( """ Failed to acquire a lock for store {}. This failure can be caused by several reasons: 1. Another 'lago' environment is using the store. 2. A stale lock was left in the store. 3. You don't have R/W permissions to the store. """.format(store_path) ) )
[docs]class LagoSubnetLeaseStoreFullException(LagoSubnetLeaseException): def __init__(self, store_range): super().__init__( dedent( """ Can't acquire subnet from range {} The store of subnets is full. You can free subnets by destroying unused lago environments' """.format(store_range) ) )
[docs]class LagoSubnetLeaseTakenException(LagoSubnetLeaseException): def __init__(self, required_subnet, lease_taken_by): super().__init__( dedent( """ Can't acquire subnet {}. The subnet is already taken by {}. """.format(required_subnet, lease_taken_by) ) )
[docs]class LagoSubnetLeaseOutOfRangeException(LagoSubnetLeaseException): def __init__(self, required_subnet, store_range): super().__init__( dedent( """ Subnet {} is not valid. Subnet should be in the range {}. """.format(required_subnet, store_range) ) )
[docs]class LagoSubnetLeaseMalformedAddrException(LagoSubnetLeaseException): def __init__(self, required_subnet): super().__init__( dedent( """ Address {} is not a valid ip address """.format(required_subnet) ) )
[docs]class LagoSubnetLeaseBadPermissionsException(LagoSubnetLeaseException): def __init__(self, store_path, prv_msg): super().__init__( dedent( """ Failed to get access to the store at {}. Please make sure that you have R/W permissions to this directory and that it exists. """.format(store_path) ), prv_msg=prv_msg, )