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


from distutils.version import LooseVersion
import logging

from f5.sdk_exception import F5SDKError
from f5.sdk_exception import UnsupportedMethod


[docs]class InvalidCommand(F5SDKError): """Raise this if command argument supplied is invalid.""" pass
[docs]class UnsupportedTmosVersion(F5SDKError): """Raise the error if a class of an API is instantiated, on a TMOS version where API was not yet implemented/supported. """ pass
[docs]class LazyAttributesRequired(F5SDKError): """Raised when a object accesses a lazy attribute that is not listed""" pass
[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 = {} for key, value in instance_dict.items(): 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 lower_attr_names =\ [la.__name__.lower() for la in container._meta_data['allowed_lazy_attributes']] if name not in lower_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(attribute) if 'Resource' not in bases: setattr(container, name, attribute) return attribute def _check_supported_versions(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 API which " \ "has not been implemented or supported " \ "in the device's TMOS version: {}. "\ "Minimum TMOS version supported is {}".format( tmos_v, minimum) 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 exec_cmd(self, command, **kwargs): 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) 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) self._check_command_parameters(**kwargs) session = self._meta_data['bigip']._meta_data['icr_session'] response = session.post( self._meta_data['uri'], json=kwargs, **requests_params) self._local_update(response.json()) return self
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
[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]