I have a class which provides various methods for retrieving information from a VMware vSphere environment.
Over the time my class got extended and became large, so I thought I should spend some time to re-design it.
Currently I am thinking of re-designing a bit in that class and move/group related methods into separate modules as this would help with keeping the whole project modular and better organized as various functionality would be covered by dedicated modules.
All my class methods rely on an attribute which is performing the connection and interaction with the VMware vSphere SDK, so in order to move my methods into dedicated modules as functions I need to make sure to pass that attribute as well.
In order to get rid of the instance methods, I was thinking of using a decorator for my functions that would simply register functions as available tasks in a class attribute, which in turn would be available to instances of that class and can be called to perform the required operation.
Now I have this updated version of my class + a decorator to be used when creating new vSphere tasks.
""" vPoller Agent module for the VMware vSphere Poller vPoller Agents are used by the vPoller Workers, which take care of establishing the connection to the vSphere hosts and do all the heavy lifting. Check the vSphere Web Services SDK API for more information on the properties you can request for any specific vSphere managed object - https://www.vmware.com/support/developer/vc-sdk/ """ import logging from functools import wraps from vconnector.core import VConnector from vpoller.client.core import VPollerClientMessage __all__ = ['VSphereAgent', 'task'] class VSphereAgent(VConnector): """ VSphereAgent class Defines methods for retrieving vSphere object properties These are the worker agents that do the actual polling from the VMware vSphere host. Extends: VConnector """ _tasks = {} @classmethod def _add_task(cls, name, function, required): cls._tasks[name] = { 'function': function, 'required': required, } def call_task(self, name, *args, **kwargs): """ Execute a vPoller task request Args: name (str): Name of the task to be executed """ if name not in self._tasks: return {'success': 1, 'msg': 'Unknown task requested'} return self._tasks[name]['function'](self, *args, **kwargs) def _discover_objects(self, properties, obj_type): """ Helper method to simplify discovery of vSphere managed objects This method is used by the '*.discover' vPoller Worker methods and is meant for collecting properties for multiple objects at once, e.g. during object discovery operation. Args: properties (list): List of properties to be collected obj_type (pyVmomi.vim.*): Type of vSphere managed object Returns: The discovered objects in JSON format """ logging.info( '[%s] Discovering %s managed objects', self.host, obj_type.__name__ ) view_ref = self.get_container_view(obj_type=[obj_type]) try: data = self.collect_properties( view_ref=view_ref, obj_type=obj_type, path_set=properties ) except Exception as e: return {'success': 1, 'msg': 'Cannot collect properties: %s' % e} view_ref.DestroyView() result = { 'success': 0, 'msg': 'Successfully discovered objects', 'result': data, } logging.debug( '[%s] Returning result from operation: %s', self.host, result ) return result def _get_object_properties(self, properties, obj_type, obj_property_name, obj_property_value, include_mors=False): """ Helper method to simplify retrieving of properties This method is used by the '*.get' vPoller Worker methods and is meant for collecting properties for a single managed object. We first search for the object with property name and value, then create a list view for this object and finally collect it's properties. Args: properties (list): List of properties to be collected obj_type pyVmomi.vim.*): Type of vSphere managed object obj_property_name (str): Property name used for searching for the object obj_property_value (str): Property value identifying the object in question Returns: The collected properties for this managed object in JSON format """ logging.info( '[%s] Retrieving properties for %s managed object of type %s', self.host, obj_property_value, obj_type.__name__ ) # Find the Managed Object reference for the requested object try: obj = self.get_object_by_property( property_name=obj_property_name, property_value=obj_property_value, obj_type=obj_type ) except Exception as e: return {'success': 1, 'msg': 'Cannot collect properties: %s' % e} if not obj: return { 'success': 1, 'msg': 'Cannot find object %s' % obj_property_value } # Create a list view for this object and collect properties view_ref = self.get_list_view(obj=[obj]) try: data = self.collect_properties( view_ref=view_ref, obj_type=obj_type, path_set=properties, include_mors=include_mors ) except Exception as e: return {'success': 1, 'msg': 'Cannot collect properties: %s' % e} view_ref.DestroyView() result = { 'success': 0, 'msg': 'Successfully retrieved object properties', 'result': data, } logging.debug( '[%s] Returning result from operation: %s', self.host, result ) return result def _object_datastore_get(self, obj_type, name): """ Helper method used for getting the datastores available to an object This method searches for the managed object with 'name' and retrieves the 'datastore' property which contains all datastores available/used by the managed object, e.g. VirtualMachine, HostSystem. Args: obj_type (pyVmomi.vim.*): Managed object type name (str): Name of the managed object, e.g. host, vm Returns: The discovered objects in JSON format """ logging.debug( '[%s] Getting datastores for %s managed object of type %s', self.host, name, obj_type.__name__ ) # Find the object by it's 'name' property # and get the datastores available/used by it data = self._get_object_properties( properties=['datastore'], obj_type=obj_type, obj_property_name='name', obj_property_value=name ) if data['success'] != 0: return data # Get the name and datastore properties from the result props = data['result'][0] obj_datastores = props['datastore'] # Get a list view of the datastores available/used by # this object and collect properties view_ref = self.get_list_view(obj=obj_datastores) result = self.collect_properties( view_ref=view_ref, obj_type=pyVmomi.vim.Datastore, path_set=['name', 'info.url'] ) view_ref.DestroyView() r = { 'success': 0, 'msg': 'Successfully discovered objects', 'result': result, } logging.debug('[%s] Returning result from operation: %s', self.host, r) return r def _object_alarm_get(self, obj_type, obj_property_name, obj_property_value): """ Helper method for retrieving alarms for a single Managed Object Args: obj_type (pyVmomi.vim.*): Type of the Managed Object obj_property_name (str): Property name used for searching for the object obj_property_value (str): Property value identifying the object in question Returns: The triggered alarms for the Managed Object """ logging.debug( '[%s] Retrieving alarms for %s managed object of type %s', self.host, obj_property_value, obj_type.__name__ ) # Get the 'triggeredAlarmState' property for the managed object data = self._get_object_properties( properties=['triggeredAlarmState'], obj_type=obj_type, obj_property_name=obj_property_name, obj_property_value=obj_property_value ) if data['success'] != 0: return data result = [] props = data['result'][0] alarms = props['triggeredAlarmState'] for alarm in alarms: a = { 'key': str(alarm.key), 'info': alarm.alarm.info.name, 'time': str(alarm.time), 'entity': alarm.entity.name, 'acknowledged': alarm.acknowledged, 'overallStatus': alarm.overallStatus, 'acknowledgedByUser': alarm.acknowledgedByUser, } result.append(a) r = { 'success': 0, 'msg': 'Successfully retrieved alarms', 'result': result, } logging.debug( '[%s] Returning result from operation: %s', self.host, r ) return r def task(name, required): """ Decorator for creating new vPoller tasks Args: name (str): Name of the vPoller task required (list): A list of required message attributes """ def decorator(function): logging.debug( 'Creating task %s at %s, requiring %s', name, function, required ) @wraps(function) def wrapper(*args, **kwargs): # # TODO: Validate message before processing # return function(*args, **kwargs) VSphereAgent._add_task( name=name, function=wrapper, required=required ) return wrapper return decorator I have already moved my vSphere Datacenter related methods as tasks using the @vpoller.agent.core.task decorator as shown below.
""" vSphere Agent Datacenter Tasks https://github.com/dnaeon/py-vpoller/tree/decorators/src/vpoller """ import pyVmomi from vpoller.agent.core import task @task(name='datacenter.discover', required=['hostname', 'name']) def datacenter_discover(agent, msg): """ Discover all vim.Datacenter managed objects Example client message would be: { "method": "datacenter.discover", "hostname": "vc01.example.org", } Example client message which also requests additional properties: { "method": "datacenter.discover", "hostname": "vc01.example.org", "properties": [ "name", "overallStatus" ] } Returns: The discovered objects in JSON format """ # Property names to be collected properties = ['name'] if 'properties' in msg and msg['properties']: properties.extend(msg['properties']) r = agent._discover_objects( properties=properties, obj_type=pyVmomi.vim.Datacenter ) return r @task(name='datacenter.get', required=['hostname', 'name']) def datacenter_get(agent, msg): """ Get properties of a single vim.Datacenter managed object Example client message would be: { "method": "datacenter.get", "hostname": "vc01.example.org", "name": "MyDatacenter", "properties": [ "name", "overallStatus" ] } Returns: The managed object properties in JSON format """ # Property names to be collected properties = ['name'] if 'properties' in msg and msg['properties']: properties.extend(msg['properties']) return agent._get_object_properties( properties=properties, obj_type=pyVmomi.vim.Datacenter, obj_property_name='name', obj_property_value=msg['name'] ) @task(name='datacenter.alarm.get', required=['hostname', 'name']) def datacenter_alarm_get(agent, msg): """ Get all alarms for a vim.Datacenter managed object Example client message would be: { "method": "datacenter.alarm.get", "hostname": "vc01.example.org", "name": "MyDatacenter" } Returns: The discovered alarms in JSON format """ result = agent._object_alarm_get( obj_type=pyVmomi.vim.Datacenter, obj_property_name='name', obj_property_value=msg['name'] ) return result Using the @vpoller.agent.core.task decorator I am now able to create new tasks in dedicated modules, which is what the main goal was.
My concerns with this approach is whether this is Pythonic (passing around self to external functions) and whether there is a better way to do it.