Source code for f5.bigip.mixins

# coding=utf-8
#
# Copyright 2015-2016 F5 Networks Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# NOTE:  Code taken from Effective Python Item 26


try:
    from collections import OrderedDict
except ImportError:
    try:
        from ordereddict import OrderedDict
    except ImportError as exc:
        message = ("Maybe you're using Python < 2.7 and do not have the "
                   "orderreddict external dependency installed.")
        raise exc(message)

from distutils.version import LooseVersion
from six import iteritems

import logging

from f5.sdk_exception import EmptyContent
from f5.sdk_exception import InvalidCommand
from f5.sdk_exception import LazyAttributesRequired
from f5.sdk_exception import MissingHttpHeader
from f5.sdk_exception import UnsupportedMethod
from f5.sdk_exception import UnsupportedTmosVersion
from f5.sdk_exception import UtilError


[docs]class ToDictMixin(object): """Convert an object's attributes to a dictionary""" traversed = {} Containers = tuple, list, set, frozenset, dict def to_dict(self): ToDictMixin.traversed = {} return self._to_dict() def _to_dict(self): result = self._traverse_dict(self.__dict__) return result def _traverse_dict(self, instance_dict): output = {} # This iteration breaks if the second value comes before # the first. We must use ordered dicts here tmp = OrderedDict(sorted(iteritems(instance_dict), key=lambda t: t[0])) for key, value in iteritems(tmp): output[key] = self._traverse(key, value) return output def _traverse(self, key, value): if isinstance(value, ToDictMixin.Containers) or\ hasattr(value, '__dict__'): if id(value) in ToDictMixin.traversed: return ToDictMixin.traversed[id(value)] else: ToDictMixin.traversed[id(value)] = ['TraversalRecord', key] if isinstance(value, ToDictMixin): return value._to_dict() elif isinstance(value, dict): return self._traverse_dict(value) elif isinstance(value, list): return [self._traverse(key, item) for item in value] elif hasattr(value, '__dict__'): return self._traverse_dict(value.__dict__) else: return value
[docs]class LazyAttributeMixin(object): """Allow attributes to be created lazily based on the allowed values""" def __getattr__(container, name): # ensure this object supports lazy attrs. cls_name = container.__class__.__name__ if 'allowed_lazy_attributes' not in container._meta_data: error_message = ('"allowed_lazy_attributes" not in', 'container._meta_data for class %s' % cls_name) raise LazyAttributesRequired(error_message) # ensure the requested attr is present attr_names = container.transform_attr_names() if name not in attr_names: error_message = "'%s' object has no attribute '%s'"\ % (container.__class__, name) raise AttributeError(error_message) # Instantiate and potentially set the attr on the object # Issue #112 -- Only call setattr here if the lazy attribute # is NOT a `Resource`. This should allow for only 1 ltm attribute # but many nat attributes just like the BIGIP device. for lazy_attribute in container._meta_data['allowed_lazy_attributes']: if name == lazy_attribute.__name__.lower(): attribute = lazy_attribute(container) bases = [base.__name__ for base in lazy_attribute.__bases__] # Doing version check per each resource container._check_supported_versions(container, attribute) if 'Resource' not in bases: setattr(container, name, attribute) return attribute def transform_attr_names(self): attr_names = \ [la.__name__.lower() for la in self._meta_data['allowed_lazy_attributes']] return attr_names def _check_supported_versions(self, container, attribute): tmos_v = container._meta_data['bigip'].tmos_version minimum = attribute._meta_data['minimum_version'] if LooseVersion(tmos_v) < LooseVersion(minimum): error = "There was an attempt to access resource: \n{}\n which " \ "is not implemented in the device's TMOS version: {}. " \ "The minimum TMOS version in which this resource *is* " \ "supported is {}".format( attribute._meta_data['uri'], tmos_v, minimum) raise UnsupportedTmosVersion(error) def _is_version_supported_method(container, method_version): """Helper method To use in instances where class methods on some resources require a specific TMOS version to run. Raises:: UnsupportedTmosVersion """ tmos_v = container._meta_data['bigip'].tmos_version if LooseVersion(tmos_v) < LooseVersion(method_version): error = "There was an attempt to use a method which " \ "has not been implemented or supported " \ "in the device's TMOS version: %s. " \ "Minimum TMOS version supported is %s" % ( tmos_v, method_version) raise UnsupportedTmosVersion(error)
[docs]class ExclusiveAttributesMixin(object): """Overrides ``__setattr__`` to remove exclusive attrs from the object.""" def __setattr__(self, key, value): """Remove any of the existing exclusive attrs from the object Objects attributes can be exclusive for example disable/enable. So we need to make sure objects only have one of these attributes at at time so that the updates won't fail. """ if '_meta_data' in self.__dict__: # Sometimes this is called prior to full object construction for attr_set in self._meta_data['exclusive_attributes']: if key in attr_set: new_set = set(attr_set) - set([key]) [self.__dict__.pop(n, '') for n in new_set] # Now set the attribute super(ExclusiveAttributesMixin, self).__setattr__(key, value)
[docs]class CommandExecutionMixin(object): """This adds command execution option on the objects. These objects do not support create, delete, load, and require a separate method of execution. Commands do not have direct mapping to an HTTP method so usage of POST and an absolute URI is required. """
[docs] def create(self, **kwargs): """Create is not supported for command execution :raises: UnsupportedOperation """ raise UnsupportedMethod( "%s does not support the create method" % self.__class__.__name__ )
[docs] def delete(self, **kwargs): """Delete is not supported for command execution :raises: UnsupportedOperation """ raise UnsupportedMethod( "%s does not support the delete method" % self.__class__.__name__ )
[docs] def load(self, **kwargs): """Load is not supported for command execution :raises: UnsupportedOperation """ raise UnsupportedMethod( "%s does not support the load method" % self.__class__.__name__ )
def _is_allowed_command(self, command): """Checking if the given command is allowed on a given endpoint.""" cmds = self._meta_data['allowed_commands'] if command not in self._meta_data['allowed_commands']: error_message = "The command value {0} does not exist" \ "Valid commands are {1}".format(command, cmds) raise InvalidCommand(error_message) def _check_command_result(self): """If command result exists run these checks.""" if self.commandResult.startswith('/bin/bash'): raise UtilError('%s' % self.commandResult.split(' ', 1)[1]) if self.commandResult.startswith('/bin/mv'): raise UtilError('%s' % self.commandResult.split(' ', 1)[1]) if self.commandResult.startswith('/bin/ls'): raise UtilError('%s' % self.commandResult.split(' ', 1)[1]) if self.commandResult.startswith('/bin/rm'): raise UtilError('%s' % self.commandResult.split(' ', 1)[1]) if 'invalid option' in self.commandResult: raise UtilError('%s' % self.commandResult) if 'Invalid option' in self.commandResult: raise UtilError('%s' % self.commandResult) if 'usage: /usr/bin/get_dossier' in self.commandResult: raise UtilError('%s' % self.commandResult)
[docs] def exec_cmd(self, command, **kwargs): """Wrapper method that can be changed in the inheriting classes.""" self._is_allowed_command(command) self._check_command_parameters(**kwargs) return self._exec_cmd(command, **kwargs)
def _exec_cmd(self, command, **kwargs): """Create a new method as command has specific requirements. There is a handful of the TMSH global commands supported, so this method requires them as a parameter. :raises: InvalidCommand """ kwargs['command'] = command self._check_exclusive_parameters(**kwargs) requests_params = self._handle_requests_params(kwargs) session = self._meta_data['bigip']._meta_data['icr_session'] response = session.post( self._meta_data['uri'], json=kwargs, **requests_params) new_instance = self._stamp_out_core() new_instance._local_update(response.json()) if 'commandResult' in new_instance.__dict__: new_instance._check_command_result() return new_instance
class FileUploadMixin(object): def _upload_file(self, filepathname, **kwargs): with open(filepathname, 'rb') as fileobj: self._upload(fileobj, **kwargs) def _upload(self, fileinterface, **kwargs): size = len(fileinterface.read()) fileinterface.seek(0) requests_params = self._handle_requests_params(kwargs) session = self._meta_data['icr_session'] chunk_size = kwargs.pop('chunk_size', 512 * 1024) start = 0 while True: file_slice = fileinterface.read(chunk_size) if not file_slice: break current_bytes = len(file_slice) if current_bytes < chunk_size: end = size else: end = start + current_bytes headers = { 'Content-Range': '%s-%s/%s' % (start, end - 1, size), 'Content-Type': 'application/octet-stream'} data = { 'data': file_slice, 'headers': headers, 'verify': False } logging.debug(data) requests_params.update(data) session.post(self.file_bound_uri, **requests_params) start += current_bytes class FileDownloadMixin(object): def _download_file(self, src, dest, **kwargs): with open(dest, 'wb') as fileobj: self._download(src, fileobj, **kwargs) def _download(self, src, fileinterface, **kwargs): requests_params = self._handle_requests_params(kwargs) session = self._meta_data['icr_session'] chunk_size = kwargs.pop('chunk_size', 512 * 1024) self.file_bound_uri = self._meta_data['uri'] + src start = 0 end = chunk_size - 1 size = 0 current_bytes = 0 while True: content_range = "%s-%s/%s" % (start, end, size) headers = { 'Content-Range': content_range, 'Content-Type': 'application/octet-stream' } data = { 'headers': headers, 'verify': False, 'stream': True } logging.debug(data) requests_params.update(data) response = session.get(self.file_bound_uri, **requests_params) if response.status_code == 200: # If the size is zero, then this is the first time through # the loop and we don't want to write data because we # haven't yet figured out the total size of the file. if size > 0: current_bytes += chunk_size for chunk in response.iter_content(chunk_size): fileinterface.write(chunk) # Once we've downloaded the entire file, we can break out of # the loop if end == size: break crange = response.headers['Content-Range'] # Determine the total number of bytes to read. if size == 0: size = int(crange.split('/')[-1]) - 1 # If the file is smaller than the chunk_size, the BigIP # will return an HTTP 400. Adjust the chunk_size down to # the total file size... if chunk_size > size: end = size # ...and pass on the rest of the code. continue start += chunk_size if (current_bytes + chunk_size) > size: end = size else: end = start + chunk_size - 1
[docs]class AsmFileMixin(object): """Mixin for manipulating files for ASM file-transfer endpoints. For ease of code maintenance this is separate from FileUploadMixin on purpose. """ def _download_file(self, filepathname): self._download(filepathname) def _download(self, filepathname): session = self._meta_data['icr_session'] with open(filepathname, 'wb') as writefh: headers = { 'Content-Type': 'application/json' } req_params = {'headers': headers, 'verify': False} response = session.get(self.file_bound_uri, **req_params) if response.status_code == 200: if 'Content-Length' not in response.headers: error_message = "The Content-Length header is not present." raise MissingHttpHeader(error_message) length = response.headers['Content-Length'] if int(length) > 0: writefh.write(response.content) else: error = "Invalid Content-Length value returned: %s ," \ "the value should be greater than 0" % length raise EmptyContent(error) def _upload_file(self, filepathname, **kwargs): with open(filepathname, 'rb') as fileobj: self._upload(fileobj, **kwargs) def _upload(self, fileinterface, **kwargs): size = len(fileinterface.read()) fileinterface.seek(0) requests_params = self._handle_requests_params(kwargs) session = self._meta_data['icr_session'] chunk_size = kwargs.pop('chunk_size', 512 * 1024) start = 0 while True: file_slice = fileinterface.read(chunk_size) if not file_slice: break current_bytes = len(file_slice) if current_bytes < chunk_size: end = size else: end = start + current_bytes headers = { 'Content-Range': '%s-%s/%s' % (start, end - 1, size), 'Content-Type': 'application/octet-stream'} data = {'data': file_slice, 'headers': headers, 'verify': False} logging.debug(data) requests_params.update(data) session.post(self.file_bound_uri, **requests_params) start += current_bytes
[docs]class DeviceMixin(object): '''Manage BigIP device cluster in a general way.'''
[docs] def get_device_info(self, bigip): '''Get device information about a specific BigIP device. :param bigip: bigip object --- device to inspect :returns: bigip object ''' coll = bigip.tm.cm.devices.get_collection() device = [device for device in coll if device.selfDevice == 'true'] assert len(device) == 1 return device[0]
[docs]class CheckExistenceMixin(object): '''In 11.6.0 some items return True on exists whether they exist or not''' def _check_existence_by_collection(self, container, item_name): '''Check existnce of item based on get collection call. :param collection: container object -- capable of get_collection() :param item_name: str -- name of item to search for in collection ''' coll = container.get_collection() for item in coll: if item.name == item_name: return True return False def _return_object(self, container, item_name): """Helper method to retrieve the object""" coll = container.get_collection() for item in coll: if item.name == item_name: return item