diff --git a/README.md b/README.md index 7c2d041..aff55f2 100755 --- a/README.md +++ b/README.md @@ -207,6 +207,7 @@ PYVMOMI 6.0 requires alternative connection coding and Python 2.7.9 minimum due ## Actions * `vsphere.get_moid` - Returns the MOID of vSphere managed entity corresponding to the specified parameters +* `vsphere.get_properties` - Retrieves some or all properties of some or all objects through the PropertyCollector. * `vsphere.get_vmconsole_urls` - Retrieves urls of the virtual machines' consoles * `vsphere.get_vms` - Retrieves the virtual machines on a vCenter Server system. It computes the union of Virtual Machine sets based on each parameter. * `vsphere.guest_dir_create` - Create a directory inside the guest. @@ -224,8 +225,8 @@ PYVMOMI 6.0 requires alternative connection coding and Python 2.7.9 minimum due * `vsphere.set_vm` - Changes configuration of a Virtual Machine. * `vsphere.vm_check_tools` - Wait for a Task to complete and returns its result. * `vsphere.vm_create_from_template` - Create a new VM from existing template. -* `vsphere.vm_env_items_get` - Retrieve list of Objects from VSphere -* `vsphere.vm_guest_info_get` - Retrieve Guest details of a VM object +* `vsphere.vm_env_items_get` - Retrieve list of Objects from VSphere (alternatively, use get_properties type= property=name) +* `vsphere.vm_guest_info_get` - Retrieve Guest details of a VM object (deprecated: use get_properties type=VirtualMachine property=guest) * `vsphere.vm_hw_barebones_create` - Create BareBones VM (CPU, Ram, Graphics Only) * `vsphere.vm_hw_basic_build` - Minstral Flow to Build Basic Server and power it on. * `vsphere.vm_hw_cpu_mem_edit` - Adjust the CPU and RAM values assigned to a Virtual Machine @@ -239,7 +240,7 @@ PYVMOMI 6.0 requires alternative connection coding and Python 2.7.9 minimum due * `vsphere.vm_hw_remove` - Removes the Virtual Machine. * `vsphere.vm_hw_scsi_controller_add` - Add SCSI HDD Controller device to VM * `vsphere.vm_hw_uuid_get` - Retrieve VM UUID -* `vsphere.vm_runtime_info_get` - Retrieves the Runtime information for a VM. +* `vsphere.vm_runtime_info_get` - Retrieves the Runtime information for a VM (deprecated: use get_properties type=VirtualMachine property=runtime) * `vsphere.wait_task` - Wait for a Task to complete and returns its result. ## Known Bugs diff --git a/actions/get_properties.py b/actions/get_properties.py new file mode 100644 index 0000000..b39162c --- /dev/null +++ b/actions/get_properties.py @@ -0,0 +1,137 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from pyVmomi import vim, vmodl # pylint: disable-msg=E0611 +from vmwarelib.actions import BaseAction + +import copy +import datetime +import json +import pyVmomi + + +class GetProperties(BaseAction): + + def run(self, type, property, id, raw, vsphere=None): + """ + Leverage the Property Collector to retrieve properties from any + Managed Object. + + Args: + - type: vimType + - properties: optional array of properties to get (default: all) + - ids: optional array of MOIDs to limit results (default: all) + - vsphere: pre-configured connection information + + Returns: + - dict: key = moid, value = dict of properties + """ + + self.establish_connection(vsphere) + return self.collect(self.si_content, type, property, id, raw) + + def collect(self, content, type, properties, ids, raw): + """ + Leverage the Property Collector to retrieve properties from any + Managed Object. + + Args: + - content: service instance content + - type: object type + - properties: optional array of properties to get (default: all) + - ids: optional array of MOIDs to limit results (default: all) + + Returns: + - dict: key = moid, value = dict of properties + """ + + vimtype = getattr(vim, type) + + rootFolder = content.rootFolder + viewMgr = content.viewManager + if not ids: + view = viewMgr.CreateContainerView(container=rootFolder, + type=[vimtype], + recursive=True) + else: + view = viewMgr.CreateListView() + for id in ids: + view.ModifyListView(add=[ + pyVmomi.VmomiSupport.GetWsdlType('urn:vim25', type)(id)]) + + traversal_spec = vmodl.query.PropertyCollector.TraversalSpec() + traversal_spec.name = 'traverseEntities' + traversal_spec.path = 'view' + traversal_spec.skip = False + traversal_spec.type = view.__class__ + + obj_spec = vmodl.query.PropertyCollector.ObjectSpec() + obj_spec.obj = view + obj_spec.skip = True + obj_spec.selectSet = [traversal_spec] + + property_spec = vmodl.query.PropertyCollector.PropertySpec() + property_spec.type = vimtype + if not properties: + property_spec.all = True + else: + property_spec.pathSet = properties + + filter_spec = vmodl.query.PropertyCollector.FilterSpec() + filter_spec.objectSet = [obj_spec] + filter_spec.propSet = [property_spec] + + rawdata = content.propertyCollector.RetrieveContents([filter_spec]) + return self.transform(ids, rawdata) if not raw else rawdata + + def jsonify_vsphere_obj(self, obj): + """JSONify a vSphere Managed/Data object.""" + class PyVmomiObjectJSONEncoder(json.JSONEncoder): + """Custom JSON encoder to encode vSphere object.""" + def __init__(self, *args, **kwargs): + super(PyVmomiObjectJSONEncoder, self).__init__(*args, **kwargs) + + def default(self, obj): # pylint: disable=method-hidden + if isinstance(obj, datetime.datetime): + return pyVmomi.Iso8601.ISO8601Format(obj) + elif isinstance(obj, pyVmomi.VmomiSupport.DataObject): + # eliminate the very annoying Dynamic fields if empty + if (obj.__dict__['dynamicType'] is None and + len(obj.__dict__['dynamicProperty']) == 0): + tmp = copy.deepcopy(obj.__dict__) + tmp.pop('dynamicType') + tmp.pop('dynamicProperty') + return tmp + return obj.__dict__ + elif isinstance(obj, pyVmomi.VmomiSupport.ManagedObject): + return unquote(obj).split(':')[-1] + elif isinstance(obj, type): + return str(obj) + return json.JSONEncoder.default(self, obj) + return json.loads(PyVmomiObjectJSONEncoder().encode(obj)) + + def transform(self, ids, rawdata): + result = {} + for obj in rawdata: + objid = unquote(obj.obj).split(':')[-1] + ps = {} + for prop in obj.propSet: + ps[unquote(prop.name)] = self.jsonify_vsphere_obj(prop.val) + result[objid] = ps + return (not ids or sorted(result.keys()) == sorted(ids), result) + + +def unquote(item): + return str(item).strip("'") diff --git a/actions/get_properties.yaml b/actions/get_properties.yaml new file mode 100644 index 0000000..06051b1 --- /dev/null +++ b/actions/get_properties.yaml @@ -0,0 +1,44 @@ +--- +name: get_properties +runner_type: python-script +description: Get properties of managed objects through the PropertyCollector. +enabled: true +entry_point: get_properties.py +parameters: + type: + type: string + description: The type of object to get. This type must be compatible with CreateContainerView. + required: true + position: 0 + enum: + - ComputeResource + - Datacenter + - Datastore + - Folder + - HostSystem + - ManagedEntity + - Network + - ResourcePool + - VirtualMachine + id: + type: array + description: MOIDs to restrict the results to. Omit to retrieve all objects. + required: false + position: 1 + property: + type: array + description: Fully qualified property within the type (dot separated), such as 'config.hardware.memoryMB' for a VirtualMachine. Omit to retrieve all properties. + required: false + position: 2 + raw: + type: boolean + description: If True, return the raw result of the PropertyCollector.RetrieveContents call. + required: false + default: false + position: 3 + vsphere: + type: string + description: Pre-Configured vsphere connection details + required: false + position: 4 + default: ~ diff --git a/tests/test_action_get_properties.py b/tests/test_action_get_properties.py new file mode 100644 index 0000000..9095801 --- /dev/null +++ b/tests/test_action_get_properties.py @@ -0,0 +1,151 @@ +# Licensed to the StackStorm, Inc ('StackStorm') under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 + +import copy +import datetime +import mock +import pyVmomi + +from get_properties import GetProperties +from pyVmomi import vim # pylint: disable-msg=E0611 +from vsphere_base_action_test_case import VsphereBaseActionTestCase + +__all__ = [ + 'GetPropertiesTestCase' +] + + +class GetPropertiesTestCase(VsphereBaseActionTestCase): + __test__ = True + action_cls = GetProperties + + transformed = { + "vm-22": { + "config.hardware.memoryMB": 10240, + "dataobj-dyn": { + "arguments": "arguments", + 'dynamicType': "Hello", + 'dynamicProperty': [], + "envVariables": ["A=B", "C=D"], + "programPath": "cmd.exe", + "workingDirectory": "/tmp" + }, + "guest.ipAddress": "10.99.0.4", + "name": "VCSA", + "network": ["network-15"], + "time": "1992-01-12T01:01:22Z" + }, + "vm-46": { + "config.hardware.memoryMB": 2048, + "dataobj-nodyn": { + "arguments": "arguments", + "envVariables": ["A=B", "C=D"], + "programPath": "cmd.exe", + "workingDirectory": "/tmp" + }, + "guest.ipAddress": "fe80::250:56ff:feb4:cfb9", + "name": "testvm", + "network": [], + "time": "1992-01-17T01:01:46Z" + } + } + + def setUp(self): + def mockDynamicProperty(name, val): + result = mock.Mock() + result.name = name + result.val = val + return result + super(GetPropertiesTestCase, self).setUp() + self._action = self.get_action_instance(self.new_config) + self._action.establish_connection = mock.Mock() + self._action.si_content = mock.Mock() + self._action.si_content.rootFolder = mock.Mock() + self._action.si_content.viewManager = mock.Mock() + self._action.si_content.viewManager.CreateContainerView = mock.Mock() + self._action.si_content.viewManager.CreateListView = mock.Mock() + self._action.si_content.propertyCollector = mock.Mock() + cmdspec22 = vim.vm.guest.ProcessManager.ProgramSpec( + arguments="arguments", + envVariables=["A=B", "C=D"], + programPath="cmd.exe", + workingDirectory='/tmp') + cmdspec46 = copy.deepcopy(cmdspec22) + cmdspec22.__dict__['dynamicType'] = 'Hello' + # mock up the raw objects used to test transform + vm22 = mock.Mock() + vm22.obj = 'vim.VirtualMachine:vm-22' + vm22.propSet = [mockDynamicProperty("config.hardware.memoryMB", 10240), + mockDynamicProperty("dataobj-dyn", cmdspec22), + mockDynamicProperty("guest.ipAddress", "10.99.0.4"), + mockDynamicProperty("name", "VCSA"), + mockDynamicProperty("network", [ + pyVmomi.VmomiSupport.GetWsdlType( + 'urn:vim25', 'Network')( + 'network-15')]), + mockDynamicProperty("time", + datetime.datetime( + 1992, 1, 12, 1, 1, 22))] + vm46 = mock.Mock() + vm46.obj = 'vim.VirtualMachine:vm-46' + vm46.propSet = [mockDynamicProperty("config.hardware.memoryMB", 2048), + mockDynamicProperty("dataobj-nodyn", cmdspec46), + mockDynamicProperty("guest.ipAddress", + "fe80::250:56ff:feb4:cfb9"), + mockDynamicProperty("name", "testvm"), + mockDynamicProperty("network", []), + mockDynamicProperty("time", + datetime.datetime( + 1992, 1, 17, 1, 1, 46))] + self.raw = [vm22, vm46] + + @mock.patch('pyVmomi.vmodl.query.PropertyCollector') + def test_simple_property_by_id(self, pc): + with mock.patch.object( + self._action.si_content.propertyCollector, 'RetrieveContents', + return_value=self.raw): + result = self._action.run(type='VirtualMachine', + id=['vm-22', 'vm-46'], + property=['does.not.exist'], raw=False) + assert self._action.si_content.viewManager.CreateListView.called + # status True because every object was found + self.assertEqual(result, (True, self.transformed)) + with mock.patch.object( + self._action.si_content.propertyCollector, 'RetrieveContents', + return_value=self.raw): + result = self._action.run(type='VirtualMachine', + id=['vm-22', 'vm-46', 'vm-47'], + property=None, raw=False) + # status False because not every object was found + self.assertEqual(result, (False, self.transformed)) + + @mock.patch('pyVmomi.vmodl.query.PropertyCollector') + def test_simple_by_type(self, pc): + with mock.patch.object( + self._action.si_content.propertyCollector, 'RetrieveContents', + return_value=self.raw): + result = self._action.run(type='VirtualMachine', id=None, + property=None, raw=False) + assert\ + self._action.si_content.viewManager.CreateContainerView.called + self.assertEqual(result, (True, self.transformed)) + + @mock.patch('pyVmomi.vmodl.query.PropertyCollector') + def test_raw_output(self, pc): + with mock.patch.object( + self._action.si_content.propertyCollector, 'RetrieveContents', + return_value=self.raw): + result = self._action.run(type='VirtualMachine', id=None, + property=None, raw=True) + self.assertNotEqual(result, self.transformed)