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__ import absolute_import

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 .config import config
from . import utils, log_utils

LOGGER = logging.getLogger(__name__)
LogTask = functools.partial(log_utils.LogTask, logger=LOGGER)
LOCK_NAME = 'subnet-lease.lock'


[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, IOError): 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 lease_file != LOCK_NAME ] 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, IOError): 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, )