diff --git a/GNUmakefile b/GNUmakefile deleted file mode 100644 index 6049283..0000000 --- a/GNUmakefile +++ /dev/null @@ -1,34 +0,0 @@ -############################################################################## -# -# Copyright (C) Zenoss, Inc. 2012, all rights reserved. -# -# This content is made available according to terms specified in -# License.zenoss under the directory where your Zenoss product is installed. -# -############################################################################## - -PYTHON=$(shell which python) -HERE=$(PWD) -ZP_DIR=$(HERE)/ZenPacks/zenoss/XenServer -LIB_DIR=$(ZP_DIR)/lib -BIN_DIR=$(ZP_DIR)/bin - -default: egg - -egg: - # setup.py will call 'make build' before creating the egg - python setup.py bdist_egg - -build: - echo - -build_old: - cd $(HERE) ; \ - PYTHONPATH="$(PYTHONPATH):$(LIB_DIR)" \ - $(PYTHON) setup.py install \ - --install-lib="$(LIB_DIR)" \ - --install-scripts="$(BIN_DIR)" - -clean: - rm -rf lib build dist *.egg-info - cd $(PYWBEM_DIR) ; rm -rf build dist *.egg-info diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..a46e5f7 --- /dev/null +++ b/README.markdown @@ -0,0 +1 @@ +Documentation can be found [on the Zenoss wiki](http://wiki.zenoss.org/ZenPack:XenServer). \ No newline at end of file diff --git a/README.mediawiki b/README.mediawiki index 3e1347a..caa0703 100644 --- a/README.mediawiki +++ b/README.mediawiki @@ -1,44 +1,335 @@ -== TODOs == - -Tracking tasks here at least until the first revision is done. - -;Musts -# XenServer RRD datasource type. -# Events. -# Create Impact ERD. -# Internal Impact adapters. -# External Impact adapters. -# Documentation. - -;Niceties -# Add CloudStack Impact integration. -# Add useful information to device overview screen. -# zProperty to disable modeling of VMs, VBDs, VIFs and vApps. -# Vocabulary for XenServerRRDDataPointInfo *_aggregation properties. -# Order component types on device screen sensibly. -# Add bond interfaces. (Bond) -# Add VLANs. (VLAN) -# Targeted remodeling based on events. -# Fix sleep(5) workaround in add_xenserver facade method. -# Screencast. - -== Scratch Pad == - -Tidbits that are useful to me while developing the ZenPack. - -;Add the XenServer endpoint. -
-d = dmd.Devices.XenServer.createInstance('xenserver1')
-d.setPerformanceMonitor('localhost')
-d.setZenProperty('zXenServerAddresses', ['192.168.66.71'])
-d.setZenProperty('zXenServerUsername', 'root')
-d.setZenProperty('zXenServerPassword', 'zenoss')
-commit()
-
-import time; time.sleep(4)
-d.collectDevice()
-
-sync()
-d.setZenProperty('zCollectorLogChanges', True)
-commit()
-
+This ZenPack provides support for monitoring [http://xenserver.org/ XenServer]. Monitoring is performed using [http://xenproject.org/developers/teams/xapi.html XenAPI]. + +== Gallery == + +xenserver_add_menu.png +xenserver_add_dialog.png +xenserver_pools.png +xenserver_hosts.png +xenserver_hostcpus.png +xenserver_pbds.png +xenserver_srs.png +xenserver_pifs.png +xenserver_vdis.png +xenserver_events.png +xenserver_impact.png +xenserver_networks.png +xenserver_vapps.png +xenserver_vms.png +xenserver_vbds.png +xenserver_vifs.png +xenserver_model_yuml.png +xenserver_impact_yuml.png + + +== Features == + +The features added by this ZenPack can be summarized as follows. They are each detailed further below. + +* Initial discovery and continual synchronization of relevant components. +* Performance monitoring. +* Event management. +* Service impact and root cause analysis. (Requires Zenoss Service Dynamics) + +=== Discovery === + +The following components will be automatically discovered through the XenServer host, username and password you provide. The properties and relationships will be continually maintained by way consuming events from the host. By default model changes will be polled every 60 seconds. This can be configured with the ''zXenServerModelInterval'' configuration property. + +[[File:xenserver_model_yuml.png|thumb|320px|Model Diagram]] + +;Pool +: Properties: HA Allow Overcommit, HA Enabled, HA Host Failures to Tolerate, CPU ID Feature Mask, HVM Memory Ratio, PV Memory Ratio, Description, Label, vSwitch Controller +: Relationships: Master Host, Default Storage Repository, Suspend Image Storage Repository, Crash Dump Storage Repository + +;Host +: Properties: API Version Major, API Version Minor, API Version Vendor, Address, Allowed Operations, Capabilities, CPU Count, CPU Speed, Edition, Enabled, Hostname, Is Pool Master, Description, Label, Scheduling Policy, Total Memory +: Relationships: Master for Pool, Suspend Image Storage Repository, Crash Dump Storage Repository, Local Cache Storage Repository, Server Device, Host CPUs, Physical Block Devices, Physical NICs, VMs + +;Host CPU (HostCPU) +: Properties: Family, Features, Flags, Model, Model Name, Number, Speed, Stepping, Vendor +: Relationships: Host + +;Physical Block Device (PBD) +: Properties: Currently Attached, Device Name, Legacy Mode, Location +: Relationships: Host, Storage Repository, Server Disk + +;Physical NIC (PIF) +: Properties: DNS Server Address, IPv4 Addresses, IPv6 Addresses, MAC Address, MTU, VLAN, Carrier, Currently Attached, Network Device, Network Device ID, Network Device Name, Disallow Unplug, IPv4 Gateway, IPv4 Configuration Mode, IPv6 Configuration Mode, IPv6 Gateway, Management, IPv4 Netmask, Physical, Primary Address Type, Speed, Vendor Name +: Relationships: Host, Network, Server Interface + +;Storage Repository (SR) +: Properties: Allowed Operations, Content Type, Local Cache Enabled, Description, Label, Physical Size, Shared, SM Type, Type +: Relationships: Virtual Disk Images, Physical Block Devices + +;Virtual Disk Image (VDI) +: Properties: Allow Caching, Allowed Operations, Is a Snapshot, Location, Managed, Missing, Description, Label, On Boot, Read Only, Sharable, Storage Lock, Type, Virtual Size +: Relationships: Storage Repository, Virtual Block Devices + +;Network +: Properties: MTU, Allowed Operations, Bridge, Default Locking Mode, Description, Label, IPv4 Begin, IPv4 End, Guest Installer Network, Host Internal Management Network, IPv4 Netmask +: Relationships: Physical NICs, Virtual NICs + +;vApp (VMAppliance) +: Properties: Allowed Operations, Description, Label +: Relationships: VMs + +;VM +: Properties: HVM Shadow Multiplier, vCPUs at Startup, Maximum vCPUs, Actions After Crash, Actions After Reboot, Actions After Shutdown, Allowed Operations, Domain Architecture, Domain ID, HA Always Run, HA Restart Priority, Is a Snapshot, Is a Template, Is a Control Domain, Is a Snapshot from VMPP, Actual Memory, Description, Label, Power State, Shutdown Delay, Start Delay, User Version, Version +: Relationships: Host, vApp, Guest Device, Virtual Block Devices, Virtual NICs + +;Virtual Block Device (VBD) +: Properties: Allowed Operations, Bootable, Currently Attached, Device Name, Empty, Mode, Storage Lock, Type, Unpluggable, User Device +: Relationships: VM, Virtual Disk Image, Guest Disk + +;Virtual NIC (VIF) +: Properties: MAC Address, Autogenerate MAC Address, MTU, Allowed Operations, Currently Attached, Device Name, IPv4 Allowed, IPv6 Allowed, Locking Mode +: Relationships: VM, Network, Guest Interface + +
+ +=== Performance Monitoring === + +The following metrics will be collected every 5 minutes by default. This can be configured with the ''zXenServerPerfInterval'' configuration property. Any metric that is available either via a XenAPI get_record or rrd_updates call can be collected by adding a new ''XenServer XenAPI'' or ''XenServer RRD'' datasource to the appropriate monitoring template. Note that as of XenServer 6.2, all available metrics are collected by default. + +[[File:xenserver_pools.png|thumb|320px|Pools]] + +;Pool +* property_haPlanExistsFor: Number of future host failures we have managed to find a plan for. Once this reaches zero any future host failures will cause the failure of protected VMs. (''number'') +* property_haHostFailuresToTolerate: Number of host failures to tolerate before the Pool is declared to be overcommitted. (''number'') + +
+[[File:xenserver_hosts.png|thumb|320px|Hosts]] + +;Host +* property_memoryOverhead: Virtualization memory overhead. (''bytes'') +* rrd_memoryTotal: Total memory. (''bytes'') +* rrd_memoryFree: Free memory. (''bytes'') +* rrd_xapiMemoryUsage: Memory allocated by xapi that is used. (''bytes'') +* rrd_xapiFreeMemory: Memory allocated by xapi that is free. (''bytes'') +* rrd_xapiLiveMemory: Memory allocated by xapi that is live. (''bytes'') +* rrd_xapiAllocation: Total memory allocated by xapi. (''bytes'') +* rrd_pifAggrRX: Total inbound throughput for all host PIFs. (''bits/sec'') +* rrd_pifAggrTX: Total outbound throughput for all host PIFs. (''bits/sec'') +* rrd_cpuAvg: Average CPU utilization for all host CPUs. (''percent'') +* rrd_loadAvg: Load average. (''processes'') +* rrd_srCacheHitsSum: Storage repository cache hit rate. (''hits/sec'') +* rrd_srCacheMissesSum: Storage repository cache miss rate. (''misses/sec'') + +
+[[File:xenserver_hostcpus.png|thumb|320px|Host CPUs]] + +;Host CPU (HostCPU) +* rrd_cpu: Average CPU utilization. (''percent'') + +
+[[File:xenserver_pbds.png|thumb|320px|Physical Block Devices]] + +;Physical Block Device (PBD) +* No metrics available. + +
+[[File:xenserver_pifs.png|thumb|320px|Physical NICs]] + +;Physical NIC (PIF) +* metric_speed: Speed of interface. (''bits/sec'') +* rrd_rx: Inbound throughput. (''bits/sec'') +* rrd_tx: Outbound throughput. (''bits/sec'') + +
+[[File:xenserver_srs.png|thumb|320px|Storage Repositories]] + +;Storage Repository (SR) +* property_physicalSize: Total physical storage size. (''bytes'') +* property_physicalUtilisation: Used physical storage. (''bytes'') +* property_virtualAllocation: Allocated physical storage. (''bytes'') + +
+[[File:xenserver_vdis.png|thumb|320px|Virtual Disk Images]] + +;Virtual Disk Image (VDI) +* property_physicalUtilisation: Used physical storage. (''bytes'') +* property_virtualSize: Total virtual storage size. (''bytes'') + +
+[[File:xenserver_networks.png|thumb|320px|Networks]] + +;Network +* No metrics available. + +
+[[File:xenserver_vapps.png|thumb|320px|vApps]] + +;vApp (VMAppliance) +* No metrics available. + +
+[[File:xenserver_vms.png|thumb|320px|VMs]] + +;VM +* property_memoryOverhead: Virtualization memory overhead. (''bytes'') +* metric_vcpusNumber: Current number of vCPUs. (''number'') +* metric_memoryActual: Guest's actual memory. (''bytes'') +* rrd_cpuAvg: Average utilization for all vCPUs. (''percent'') +* rrd_memory: Memory currently allocated to VM. (''bytes'') +* rrd_vifRXSum: Total inbound throughput for all VM VIFs. (''bits/sec'') +* rrd_vifTXSum: Total outbound throughput for all VM VIFs. (''bits/sec'') +* rrd_vbdReadSum: Total read rate for all VM VBDs. (''bytes/sec'') +* rrd_vbdWriteSum: Total write rate for all VM VBDs. (''bytes/sec'') +* rrd_memoryInternalFree: Memory used as reported by the guest agent. (''bytes'') +* rrd_memoryTarget: Target of VM balloon driver. (''bytes'') + +
+[[File:xenserver_vbds.png|thumb|320px|Virtual Block Devices]] + +;Virtual Block Device (VBD) +* rrd_read: Read rate. (''bytes/sec'') +* rrd_write: Write rate. (''bytes/sec'') + +
+[[File:xenserver_vifs.png|thumb|320px|Virtual NICs]] + +;Virtual NIC (VIF) +* rrd_rx: Inbound throughput. (''bits/sec'') +* rrd_tx: Outbound throughput. (''bits/sec'') + +
+ +=== Event Management === + +Zenoss watches for XenAPI ''messages'' and creates Zenoss events when they occur. XenAPI messages are the system alerts you see in XenCenter. By default, Zenoss will poll for new messages every 60 seconds. This can be configured with the ''zXenServerEventsInterval'' configuration property. + +[[File:xenserver_events.png|thumb|320px|Events]] + +The created Zenoss events will have the following fields set. + +;Standard Zenoss Event Fields +* device: Set to the XenServer endpoint device in the /XenServer device class. +* component: Looked up in from Zenoss components using the message's ''obj_uuid'' value. +* summary: In preference order: message's body field then name field then "no body or name provided". +* severity: Mapped from message's priority field using the map below. +* eventKey: Message's uuid value. +* eventClassKey: Literally "XenServerMessage". + +; Additional Event Fields +* xenserver_name: Message's name value. +* xenserver_cls: Message's cls value. + +;XenAPI Message Priority to Zenoss Event Severity Mapping +* Priority 1: Critical +* Priority 2: Error +* Priority 3: Warning +* Priority 4: Clear +* Priority 5: Info + +
+ +=== Service Impact and Root Cause Analysis === + +When combined with the Zenoss Service Dynamics product, this ZenPack adds built-in service impact and root cause analysis capabilities for services running on XenServer. The service impact relationships shown in the diagram and described below are automatically added. These will be included in any services that contain one or more of the explicitly mentioned components. + +[[File:xenserver_impact_yuml.png|thumb|320px|Impact Relationship Diagram]] + +;Internal Impact Relationships +* XenAPI (endpoint) access failure impacts all hosts. +* Host failure impacts the host's pool and any resident VMs. +* Network failure impacts all related virtual NICs. +* Physical block device failure impacts its host and storage repository. +* Physical NIC failure impacts its host and network. +* Pool failure impacts any contained VMs. +* Storage repository failure impacts any pools and hosts for which it is a default, suspend image, crash dump or local cache repository. +* Virtual block device failure impacts its VM. +* Virtual disk image failure impacts any related virtual block devices. +* Virtual NIC failure impacts its VM. +* VM failure impacts its vApp. + +
+[[File:xenserver_impact.png|thumb|320px|XenServer Hosted VM in a Service]] + +;External Impact Relationships +* Underlying server failure impacts the associated XenServer host. +* Underlying server disk failure impacts the associated XenServer physical block device. +* Underlying server interface failure impacts the associated XenServer physical NIC. +* XenServer VM failure impacts the associated guest device. +* XenServer virtual block device failure impacts the associated guest hard disk. +* XenServer virtual NIC failure impacts the associated guest interface. +* XenServer host failure impacts the associated CloudStack host. +* XenServer VM failure impacts the associated CloudStack router VM, system VM or regular VM. + +
+ +== Usage == + +=== Adding XenServer Endpoint === + +Use the following steps to start monitoring XenServer using the Zenoss web interface. + +# Navigate to the Infrastructure page. [[File:xenserver_add_menu.png|thumb|320px|Add Menu Item]] +# Choose ''Add XenServer Endpoint'' from the add device button.
+# Fill out the form. [[File:xenserver_add_dialog.png|thumb|320px|Add Dialog]] +#* ''Name'' can be anything you want. +#* ''Address'' must be resolvable and accessible from the collector server chosen in the ''Collector'' field. +#* ''Username'' and ''Password'' are the same as what you'd use in XenCenter. +# Click ''ADD''. + +
+ +---- + +Alternatively you can use zenbatchload to add XenServer endpoints from the command line. To do this, you must create a file with contents similar to the following. Replace all values in angle brackets with your values minus the brackets. Multiple endpoints can be added under the same ''/Devices/XenServer'' section. + + +/Devices/XenServer +test-xenserver-pool zXenServerAddresses=[<address>], zXenServerUsername='<username>', zXenServerPassword='<password>' + + +You can then load the endpoint(s) with the following command. + + +zenbatchload + + +== Installed Items == + +Installing this ZenPack will add the following items to your Zenoss system. + +;Configuration Properties +* zXenServerAddresses +* zXenServerUsername +* zXenServerPassword +* zXenServerPerfInterval: Metric collection interval in seconds. Default is 300. +* zXenServerModelInterval: Model update interval in seconds. Default is 60. +* zXenServerEventsInterval: Events collection interval in seconds. Default is 60. + +;Device Classes +* /XenServer + +;Modeler Plugins +* zenoss.XenServer + +;Datasource Types +* XenServer XenAPI +* XenServer RRD + +;Monitoring Templates (all in /XenServer) +* Endpoint +* Host +* HostCPU +* Network +* PBD +* PIF +* Pool +* SR +* VBD +* VDI +* VIF +* VM +* VMAppliance +* VMGuest + +;Event Classes +* /XenServer + +;Event Class Mappings +* XenServerCollectionError (in /Status) +* XenServerCollectionSuccess (in /Status) +* XenServerMessage (in /XenServer) diff --git a/ZenPacks/zenoss/XenServer/Endpoint.py b/ZenPacks/zenoss/XenServer/Endpoint.py index 8e94e82..0cc1d11 100644 --- a/ZenPacks/zenoss/XenServer/Endpoint.py +++ b/ZenPacks/zenoss/XenServer/Endpoint.py @@ -12,6 +12,7 @@ from zope.interface import implements from Products.ZenModel.Device import Device +from Products.ZenModel.ZenossSecurity import ZEN_VIEW from Products.ZenRelations.RelSchema import ToManyCont, ToOne from Products.Zuul.form import schema from Products.Zuul.infos.device import DeviceInfo @@ -27,7 +28,7 @@ class Endpoint(Device): Model class for Endpoint. ''' - meta_type = portal_type = 'Endpoint' + meta_type = portal_type = 'XenServerEndpoint' _relations = Device._relations + ( ('hosts', ToManyCont(ToOne, MODULE_NAME['Host'], 'endpoint')), @@ -38,6 +39,15 @@ class Endpoint(Device): ('vms', ToManyCont(ToOne, MODULE_NAME['VM'], 'endpoint')), ) + factory_type_information = ({ + 'actions': ({ + 'id': 'events', + 'name': 'Events', + 'action': 'viewEvents', + 'permissions': (ZEN_VIEW,), + },), + },) + @property def xenserver_addresses(self): ''' @@ -53,6 +63,12 @@ def xenserver_addresses(self): return addresses + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/xenserver.png' + class IEndpointInfo(IDeviceInfo): ''' diff --git a/ZenPacks/zenoss/XenServer/Host.py b/ZenPacks/zenoss/XenServer/Host.py index 1cf04cb..c513f7b 100644 --- a/ZenPacks/zenoss/XenServer/Host.py +++ b/ZenPacks/zenoss/XenServer/Host.py @@ -8,6 +8,8 @@ # ###################################################################### +import itertools + from zope.component import adapts from zope.interface import implements @@ -21,6 +23,9 @@ PooledComponent, IPooledComponentInfo, PooledComponentInfo, RelationshipInfoProperty, RelationshipLengthProperty, updateToMany, updateToOne, + id_from_ref, ids_from_refs, int_or_none, float_or_none, + findIpInterfacesByMAC, + require_zenpack, ) @@ -31,7 +36,7 @@ class Host(PooledComponent): meta_type = portal_type = 'XenServerHost' - xapi_metrics_ref = None + xenapi_metrics_ref = None api_version_major = None api_version_minor = None api_version_vendor = None @@ -49,7 +54,7 @@ class Host(PooledComponent): memory_total = None _properties = PooledComponent._properties + ( - {'id': 'xapi_metrics_ref', 'type': 'string', 'mode': 'w'}, + {'id': 'xenapi_metrics_ref', 'type': 'string', 'mode': 'w'}, {'id': 'api_version_major', 'type': 'string', 'mode': 'w'}, {'id': 'api_version_minor', 'type': 'string', 'mode': 'w'}, {'id': 'api_version_vendor', 'type': 'string', 'mode': 'w'}, @@ -83,6 +88,73 @@ class Host(PooledComponent): def is_pool_master(self): return self.master_for() is not None + @property + def ipv4_addresses(self): + return tuple(itertools.chain.from_iterable( + x.ipv4_addresses for x in self.pifs() if x.ipv4_addresses)) + + @property + def mac_addresses(self): + return tuple(x.macaddress for x in self.pifs() if x.macaddress) + + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI host ref and properties. + ''' + if 'uuid' not in properties: + return { + 'relname': 'hosts', + 'id': id_from_ref, + } + + title = properties.get('name_label') or properties.get('hostname') + + cpu_info = properties.get('cpu_info', {}) + + cpu_speed = float_or_none(cpu_info.get('speed')) + if cpu_speed: + cpu_speed = cpu_speed * 1048576 # Convert from MHz to Hz. + + return { + 'relname': 'hosts', + 'id': id_from_ref(ref), + 'title': title, + 'xenapi_ref': ref, + 'xenapi_uuid': properties.get('uuid'), + 'xenapi_metrics_ref': properties.get('metrics'), + 'api_version_major': properties.get('API_version_major'), + 'api_version_minor': properties.get('API_version_minor'), + 'api_version_vendor': properties.get('API_version_vendor'), + 'address': properties.get('address'), + 'allowed_operations': properties.get('allowed_operations'), + 'capabilities': properties.get('capabilities'), + 'cpu_count': int_or_none(cpu_info.get('cpu_count')), + 'cpu_speed': cpu_speed, + 'edition': properties.get('edition'), + 'enabled': properties.get('enabled'), + 'hostname': properties.get('hostname'), + 'name_description': properties.get('name_description'), + 'name_label': properties.get('name_label'), + 'sched_policy': properties.get('sched_policy'), + 'setVMs': ids_from_refs(properties.get('resident_VMs', [])), + 'setSuspendImageSR': id_from_ref(properties.get('suspend_image_sr')), + 'setCrashDumpSR': id_from_ref(properties.get('crash_dump_sr')), + 'setLocalCacheSR': id_from_ref(properties.get('local_cache_sr')), + } + + @classmethod + def objectmap_metrics(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI host ref and host_metrics + properties. + ''' + return { + 'relname': 'hosts', + 'id': id_from_ref(ref), + 'memory_total': int_or_none(properties.get('memory_total')), + } + def getVMs(self): ''' Return a sorted list of each vm id related to this host. @@ -197,8 +269,49 @@ def xenrrd_prefix(self): Return prefix under which XenServer stores RRD data about this component. ''' - if self.xapi_uuid: - return ('host', self.xapi_uuid, '') + if self.xenapi_uuid: + return ('host', self.xenapi_uuid, '') + + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/host.png' + + def server_device(self): + ''' + Return the associated device on which this host runs. + + Zenoss may also be monitoring the XenServer host as a normal + Linux server. This method will attempt to find that device by + matching the XenServer host's PIF MAC addresses then IP + addresses with those of other monitored devices. + ''' + macaddresses = [x.macaddress for x in self.pifs() if x.macaddress] + if macaddresses: + for iface in findIpInterfacesByMAC(self.dmd, macaddresses): + return iface.device() + + if self.address: + ip = self.endpoint.getNetworkRoot().findIp(self.address) + if ip: + device = ip.device() + if device: + return device + + @require_zenpack('ZenPacks.zenoss.CloudStack') + def cloudstack_host(self): + ''' + Return the associated CloudStack host. + ''' + from ZenPacks.zenoss.CloudStack.Host import Host + + try: + return Host.findByIP(self.dmd, self.ipv4_addresses) + except AttributeError: + # The CloudStack Host class didn't gain the findByIP method + # until version 1.1 of the ZenPack. + pass class IHostInfo(IPooledComponentInfo): @@ -227,6 +340,7 @@ class IHostInfo(IPooledComponentInfo): suspend_image_sr = schema.Entity(title=_t(u'Suspend Image Storage Repository')) crash_dump_sr = schema.Entity(title=_t(u'Crash Dump Storage Repository')) local_cache_sr = schema.Entity(title=_t(u'Local Cache Storage Repository')) + server_device = schema.Entity(title=_t(u'Server Device')) hostcpu_count = schema.Int(title=_t(u'Number of Host CPUs')) pbd_count = schema.Int(title=_t(u'Number of Block Devices')) @@ -242,7 +356,7 @@ class HostInfo(PooledComponentInfo): implements(IHostInfo) adapts(Host) - xapi_metrics_ref = ProxyProperty('xapi_metrics_ref') + xenapi_metrics_ref = ProxyProperty('xenapi_metrics_ref') api_version_major = ProxyProperty('api_version_major') api_version_minor = ProxyProperty('api_version_minor') api_version_vendor = ProxyProperty('api_version_vendor') @@ -264,8 +378,43 @@ class HostInfo(PooledComponentInfo): suspend_image_sr = RelationshipInfoProperty('suspend_image_sr') crash_dump_sr = RelationshipInfoProperty('crash_dump_sr') local_cache_sr = RelationshipInfoProperty('local_cache_sr') + server_device = RelationshipInfoProperty('server_device') hostcpu_count = RelationshipLengthProperty('hostcpus') pbd_count = RelationshipLengthProperty('pbds') pif_count = RelationshipLengthProperty('pifs') vm_count = RelationshipLengthProperty('vms') + + +class DeviceLinkProvider(object): + ''' + Provides a link to this host on the overview screen of the Linux + server device underlying this host. + ''' + def __init__(self, device): + self.device = device + + def getExpandedLinks(self): + links = [] + + host = self.device.xenserver_host() + if host: + endpoint = host.endpoint() + links.append( + 'XenServer Host: {} on {}'.format( + host.getPrimaryUrlPath(), + host.titleOrId(), + endpoint.getPrimaryUrlPath(), + endpoint.titleOrId())) + + vm = self.device.xenserver_vm() + if vm: + endpoint = vm.endpoint() + links.append( + 'XenServer VM: {} on {}'.format( + vm.getPrimaryUrlPath(), + vm.titleOrId(), + endpoint.getPrimaryUrlPath(), + endpoint.titleOrId())) + + return links diff --git a/ZenPacks/zenoss/XenServer/HostCPU.py b/ZenPacks/zenoss/XenServer/HostCPU.py index 344f041..c3aab32 100644 --- a/ZenPacks/zenoss/XenServer/HostCPU.py +++ b/ZenPacks/zenoss/XenServer/HostCPU.py @@ -20,6 +20,7 @@ from ZenPacks.zenoss.XenServer.utils import ( PooledComponent, IPooledComponentInfo, PooledComponentInfo, RelationshipInfoProperty, + id_from_ref, int_or_none, ) @@ -56,12 +57,48 @@ class HostCPU(PooledComponent): ('host', ToOne(ToManyCont, MODULE_NAME['Host'], 'hostcpus')), ) + @classmethod + def objectmap(self, ref, properties): + ''' + Return an ObjectMap given XenAPI host_cpu ref and properties. + ''' + if 'uuid' not in properties: + return { + 'compname': 'hosts/{}'.format(id_from_ref(properties['parent'])), + 'relname': 'hostcpus', + 'id': id_from_ref(ref), + } + + title = properties.get('number') or properties['uuid'] + + cpu_speed = int_or_none(properties.get('speed')) + if cpu_speed: + cpu_speed = cpu_speed * 1048576 # Convert from MHz to Hz. + + return { + 'compname': 'hosts/{}'.format(id_from_ref(properties.get('host'))), + 'relname': 'hostcpus', + 'id': id_from_ref(ref), + 'title': title, + 'xenapi_ref': ref, + 'xenapi_uuid': properties.get('uuid'), + 'family': int_or_none(properties.get('family')), + 'features': properties.get('features'), + 'flags': properties.get('flags'), + 'model': int_or_none(properties.get('model')), + 'modelname': properties.get('modelname'), + 'number': int_or_none(properties.get('number')), + 'speed': cpu_speed, + 'stepping': int_or_none(properties.get('stepping')), + 'vendor': properties.get('vendor'), + } + def xenrrd_prefix(self): ''' Return prefix under which XenServer stores RRD data about this component. ''' - host_uuid = self.host().xapi_uuid + host_uuid = self.host().xenapi_uuid if host_uuid and self.number is not None: return ('host', host_uuid, ''.join(('cpu', str(self.number)))) diff --git a/ZenPacks/zenoss/XenServer/Network.py b/ZenPacks/zenoss/XenServer/Network.py index c585264..7bd70f1 100644 --- a/ZenPacks/zenoss/XenServer/Network.py +++ b/ZenPacks/zenoss/XenServer/Network.py @@ -21,6 +21,7 @@ PooledComponent, IPooledComponentInfo, PooledComponentInfo, RelationshipLengthProperty, updateToMany, + id_from_ref, ids_from_refs, to_boolean, ) @@ -63,6 +64,42 @@ class Network(PooledComponent): ('vifs', ToMany(ToOne, MODULE_NAME['VIF'], 'network')), ) + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI network ref and properties. + ''' + if 'uuid' not in properties: + return { + 'relname': 'networks', + 'id': id_from_ref(ref), + } + + title = properties.get('name_label') or properties['uuid'] + + other_config = properties.get('other_config', {}) + + return { + 'relname': 'networks', + 'id': id_from_ref(ref), + 'title': title, + 'xenapi_ref': ref, + 'xenapi_uuid': properties.get('uuid'), + 'mtu': properties.get('MTU'), + 'allowed_operations': properties.get('allowed_operations'), + 'bridge': properties.get('bridge'), + 'default_locking_mode': properties.get('default_locking_mode'), + 'name_description': properties.get('name_description'), + 'name_label': properties.get('name_label'), + 'ipv4_begin': other_config.get('ip_begin'), + 'ipv4_end': other_config.get('ip_end'), + 'is_guest_installer_network': to_boolean(other_config.get('is_guest_installer_network')), + 'is_host_internal_management_network': to_boolean(other_config.get('is_host_internal_management_network')), + 'ipv4_netmask': other_config.get('ipv4_netmask'), + 'setPIFs': ids_from_refs(properties.get('PIFs', [])), + 'setVIFs': ids_from_refs(properties.get('VIFs', [])), + } + def getPIFs(self): ''' Return a sorted list of ids in pifs relationship. @@ -112,8 +149,14 @@ def xenrrd_prefix(self): ''' # This is a guess at future support. XenServer 6.2 doesn't have # any RRD data for networks. - if self.xapi_uuid: - return ('network', self.xapi_uuid, '') + if self.xenapi_uuid: + return ('network', self.xenapi_uuid, '') + + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/virtual-network-interface.png' class INetworkInfo(IPooledComponentInfo): diff --git a/ZenPacks/zenoss/XenServer/PBD.py b/ZenPacks/zenoss/XenServer/PBD.py index 8ecd7ab..97b944f 100644 --- a/ZenPacks/zenoss/XenServer/PBD.py +++ b/ZenPacks/zenoss/XenServer/PBD.py @@ -12,6 +12,7 @@ from zope.interface import implements from Products.ZenRelations.RelSchema import ToMany, ToManyCont, ToOne +from Products.ZenUtils.Utils import prepId from Products.Zuul.catalog.paths import DefaultPathReporter, relPath from Products.Zuul.form import schema from Products.Zuul.infos import ProxyProperty @@ -22,6 +23,7 @@ PooledComponent, IPooledComponentInfo, PooledComponentInfo, RelationshipInfoProperty, updateToOne, + id_from_ref, to_boolean, ) @@ -49,6 +51,38 @@ class PBD(PooledComponent): ('sr', ToOne(ToMany, MODULE_NAME['SR'], 'pbds')), ) + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI PBD ref and properties. + ''' + if 'uuid' not in properties: + return { + 'compname': 'hosts/{}'.format(id_from_ref(properties['parent'])), + 'relname': 'pbds', + 'id': id_from_ref(ref), + } + + device_config = properties.get('device_config', {}) + + title = device_config.get('location') or \ + device_config.get('device') or \ + properties['uuid'] + + return { + 'compname': 'hosts/{}'.format(id_from_ref(properties.get('host'))), + 'relname': 'pbds', + 'id': id_from_ref(ref), + 'title': title, + 'xenapi_ref': ref, + 'xenapi_uuid': properties.get('uuid'), + 'currently_attached': properties.get('currently_attached'), + 'dc_device': device_config.get('device'), + 'dc_legacy_mode': to_boolean(device_config.get('legacy_mode')), + 'dc_location': device_config.get('location'), + 'setSR': id_from_ref(properties.get('SR')), + } + def getSR(self): ''' Return SR id or None. @@ -78,9 +112,31 @@ def xenrrd_prefix(self): ''' # This is a guess at future support. XenServer 6.2 doesn't have # any RRD data for PBDs. - host_uuid = self.host().xapi_uuid - if host_uuid and self.xapi_uuid: - return ('host', host_uuid, '_'.join(('pbd', self.xapi_uuid))) + host_uuid = self.host().xenapi_uuid + if host_uuid and self.xenapi_uuid: + return ('host', host_uuid, '_'.join(('pbd', self.xenapi_uuid))) + + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/virtual-disk.png' + + def server_disk(self): + ''' + Return the server disk underlying this PBD. + + The host on which this PBD resides may also be monitored as a + normal Linux server. Attempt to find that server and its disk + that's associated with this PBD. + ''' + if not self.dc_device or not self.dc_device.startswith('/dev'): + return + + server_device = self.host().server_device() + if server_device: + return server_device.hw.harddisks._getOb( + prepId(self.dc_device.replace('/dev/', '', 1)), None) class IPBDInfo(IPooledComponentInfo): @@ -90,6 +146,7 @@ class IPBDInfo(IPooledComponentInfo): host = schema.Entity(title=_t(u'Host')) sr = schema.Entity(title=_t(u'Storage Repository')) + server_disk = schema.Entity(title=_t(u'Server Disk')) currently_attached = schema.Bool(title=_t(u'Currently Attached')) dc_device = schema.TextLine(title=_t(u'Device Name')) @@ -107,6 +164,7 @@ class PBDInfo(PooledComponentInfo): host = RelationshipInfoProperty('host') sr = RelationshipInfoProperty('sr') + server_disk = RelationshipInfoProperty('server_disk') currently_attached = ProxyProperty('currently_attached') dc_device = ProxyProperty('dc_device') diff --git a/ZenPacks/zenoss/XenServer/PIF.py b/ZenPacks/zenoss/XenServer/PIF.py index bf88a45..7a1b01f 100644 --- a/ZenPacks/zenoss/XenServer/PIF.py +++ b/ZenPacks/zenoss/XenServer/PIF.py @@ -8,10 +8,14 @@ # ###################################################################### +import logging +LOG = logging.getLogger('zen.XenServer') + from zope.component import adapts from zope.interface import implements from Products.ZenRelations.RelSchema import ToMany, ToManyCont, ToOne +from Products.ZenUtils.Utils import prepId from Products.Zuul.catalog.paths import DefaultPathReporter, relPath from Products.Zuul.form import schema from Products.Zuul.infos import ProxyProperty @@ -22,6 +26,7 @@ PooledComponent, IPooledComponentInfo, PooledComponentInfo, RelationshipInfoProperty, updateToOne, + id_from_ref, int_or_none, ) @@ -31,7 +36,7 @@ class PIF(PooledComponent): ''' meta_type = portal_type = 'XenServerPIF' - xapi_metrics_ref = None + xenapi_metrics_ref = None dns = None ipv4_addresses = None ipv6_addresses = None @@ -56,7 +61,7 @@ class PIF(PooledComponent): vendor_name = None _properties = PooledComponent._properties + ( - {'id': 'xapi_metrics_ref', 'type': 'string', 'mode': 'w'}, + {'id': 'xenapi_metrics_ref', 'type': 'string', 'mode': 'w'}, {'id': 'dns', 'type': 'string', 'mode': 'w'}, {'id': 'ipv4_addresses', 'type': 'lines', 'mode': 'w'}, {'id': 'ipv6_addresses', 'type': 'lines', 'mode': 'w'}, @@ -86,6 +91,111 @@ class PIF(PooledComponent): ('network', ToOne(ToMany, MODULE_NAME['Network'], 'pifs')), ) + _catalogs = dict({ + 'PIFCatalog': { + 'deviceclass': '/XenServer', + 'indexes': { + 'ipv4_addresses': {'type': 'keyword'}, + 'mac_addresses': {'type': 'keyword'}, + }, + }, + }, **PooledComponent._catalogs) + + @property + def mac_addresses(self): + return (self.macaddress,) + + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI PIF ref and properties. + ''' + if 'uuid' not in properties: + return { + 'compname': 'hosts/{}'.format(id_from_ref(properties['parent'])), + 'relname': 'pifs', + 'id': id_from_ref(ref), + } + + title = properties.get('device') or properties['uuid'] + + # IP is a single string whereas IPv6 is a list. + ipv4_addresses = [x for x in [properties.get('IP')] if x] + ipv6_addresses = [x for x in properties.get('IPv6', []) if x] + + vlan = properties.get('VLAN') + if vlan == '-1': + vlan = None + + return { + 'compname': 'hosts/{}'.format(id_from_ref(properties.get('host'))), + 'relname': 'pifs', + 'id': id_from_ref(ref), + 'title': title, + 'xenapi_ref': ref, + 'xenapi_metrics_ref': properties.get('metrics'), + 'xenapi_uuid': properties.get('uuid'), + 'dns': properties.get('dns'), + 'ipv4_addresses': ipv4_addresses, + 'ipv6_addresses': ipv6_addresses, + 'macaddress': properties.get('MAC'), + 'mtu': properties.get('MTU'), + 'vlan': vlan, + 'currently_attached': properties.get('currently_attached'), + 'pif_device': properties.get('device'), + 'disallow_unplug': properties.get('disallow_unplug'), + 'ipv4_gateway': properties.get('gateway'), + 'ipv4_configuration_mode': properties.get('ip_configuration_mode'), + 'ipv6_configuration_mode': properties.get('ipv6_configuration_mode'), + 'ipv6_gateway': properties.get('ipv6_gateway'), + 'management': properties.get('management'), + 'ipv4_netmask': properties.get('netmask'), + 'physical': properties.get('physical'), + 'primary_address_type': properties.get('primary_address_type'), + 'setNetwork': id_from_ref(properties.get('network')), + } + + @classmethod + def objectmap_metrics(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI host ref and host_metrics + properties. + ''' + + # Extract nested refs. + ref, host_ref = ref + + speed = int_or_none(properties.get('speed')) + if speed: + speed = speed * 1e6 # Convert from Mbps to bps. + + return { + 'compname': 'hosts/{}'.format(id_from_ref(host_ref)), + 'relname': 'pifs', + 'id': id_from_ref(ref), + 'carrier': properties.get('carrier'), + 'pif_device_id': properties.get('device_id'), + 'pif_device_name': properties.get('device_name'), + 'speed': speed, + 'vendor_name': properties.get('vendor_name'), + } + + @classmethod + def findByIP(cls, dmd, ipv4_addresses): + ''' + Return the first PIF matching one of ipv4_addresses. + ''' + return next(cls.search( + dmd, 'PIFCatalog', ipv4_addresses=ipv4_addresses), None) + + @classmethod + def findByMAC(cls, dmd, mac_addresses): + ''' + Return the first PIF matching one of mac_addresses. + ''' + return next(cls.search( + dmd, 'PIFCatalog', mac_addresses=mac_addresses), None) + def getNetwork(self): ''' Return network id or None. @@ -113,10 +223,32 @@ def xenrrd_prefix(self): Return prefix under which XenServer stores RRD data about this component. ''' - host_uuid = self.host().xapi_uuid + host_uuid = self.host().xenapi_uuid if host_uuid and self.pif_device: return ('host', host_uuid, '_'.join(('pif', self.pif_device))) + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/virtual-network-interface.png' + + def server_interface(self): + ''' + Return the server interface underlying this PIF. + + The host on which this PIF resides may also be monitored as a + normal Linux server. Attempt to find that server and its + interface that's associated with this PIF. + ''' + if not self.pif_device: + return + + server_device = self.host().server_device() + if server_device: + return server_device.os.interfaces._getOb( + prepId(self.pif_device), None) + class IPIFInfo(IPooledComponentInfo): ''' @@ -125,6 +257,7 @@ class IPIFInfo(IPooledComponentInfo): host = schema.Entity(title=_t(u'Host')) network = schema.Entity(title=_t(u'Network')) + server_interface = schema.Entity(title=_t(u'Server Interface')) dns = schema.TextLine(title=_t(u'DNS Server Address')) ipv4_addresses = schema.TextLine(title=_t(u'IPv4 Addresses')) @@ -160,8 +293,9 @@ class PIFInfo(PooledComponentInfo): host = RelationshipInfoProperty('host') network = RelationshipInfoProperty('network') + server_interface = RelationshipInfoProperty('server_interface') - xapi_metrics_ref = ProxyProperty('xapi_metrics_ref') + xenapi_metrics_ref = ProxyProperty('xenapi_metrics_ref') dns = ProxyProperty('dns') ipv4_addresses = ProxyProperty('ipv4_addresses') ipv6_addresses = ProxyProperty('ipv6_addresses') diff --git a/ZenPacks/zenoss/XenServer/Pool.py b/ZenPacks/zenoss/XenServer/Pool.py index 9337b9e..c4d70cf 100644 --- a/ZenPacks/zenoss/XenServer/Pool.py +++ b/ZenPacks/zenoss/XenServer/Pool.py @@ -19,8 +19,9 @@ from ZenPacks.zenoss.XenServer import CLASS_NAME, MODULE_NAME from ZenPacks.zenoss.XenServer.utils import ( BaseComponent, IBaseComponentInfo, BaseComponentInfo, - RelationshipInfoProperty, + RelationshipInfoProperty, RelationshipLengthProperty, updateToOne, + id_from_ref, int_or_none, ) @@ -61,6 +62,42 @@ class Pool(BaseComponent): ('crash_dump_sr', ToOne(ToMany, MODULE_NAME['SR'], 'crash_dump_for_pools')), ) + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI pool ref and properties. + ''' + if 'uuid' not in properties: + return { + 'relname': 'pools', + 'id': id_from_ref(ref), + } + + pool_title = properties.get('name_label') or 'default' + + other_config = properties.get('other_config', {}) + + return { + 'relname': 'pools', + 'id': id_from_ref(ref), + 'title': pool_title, + 'xenapi_ref': ref, + 'xenapi_uuid': properties.get('uuid'), + 'ha_allow_overcommit': properties.get('ha_allow_overcommit'), + 'ha_enabled': properties.get('ha_enabled'), + 'ha_host_failures_to_tolerate': int_or_none(properties.get('ha_host_failures_to_tolerate')), + 'name_description': properties.get('name_description'), + 'name_label': properties.get('name_label'), + 'oc_cpuid_feature_mask': other_config.get('cpuid_feature_mask'), + 'oc_memory_ratio_hvm': other_config.get('memory-ratio-hvm'), + 'oc_memory_ratio_pv': other_config.get('memory-ratio-pv'), + 'vswitch_controller': properties.get('vswitch_controller'), + 'setMaster': id_from_ref(properties.get('master')), + 'setDefaultSR': id_from_ref(properties.get('default_SR')), + 'setSuspendImageSR': id_from_ref(properties.get('suspend_image_SR')), + 'setCrashDumpSR': id_from_ref(properties.get('crash_dump_SR')), + } + def getMaster(self): ''' Return Host id or None. @@ -149,6 +186,24 @@ def setCrashDumpSR(self, sr_id): type_=CLASS_NAME['SR'], id_=sr_id) + def hosts(self): + ''' + Return the hosts belonging to this pool. + + This assumes that we only manage one pool per endpoint and + therefore returns all known hosts. + ''' + return self.endpoint().hosts() + + def vms(self): + ''' + Return the VMs belonging to this pool. + + This assumes that we only manage one pool per endpoint and + therefore returns all known VMs. + ''' + return self.endpoint().vms() + def xenrrd_prefix(self): ''' Return prefix under which XenServer stores RRD data about this @@ -156,8 +211,14 @@ def xenrrd_prefix(self): ''' # This is a guess at future support. XenServer 6.2 doesn't have # any RRD data for pools. - if self.xapi_uuid: - return ('pool', self.xapi_uuid, '') + if self.xenapi_uuid: + return ('pool', self.xenapi_uuid, '') + + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/cluster.png' class IPoolInfo(IBaseComponentInfo): @@ -180,6 +241,9 @@ class IPoolInfo(IBaseComponentInfo): name_label = schema.TextLine(title=_t(u'Label')) vswitch_controller = schema.TextLine(title=_t(u'vSwitch Controller')) + host_count = schema.Int(title=_t(u'Number of Hosts')) + vm_count = schema.Int(title=_t(u'Number of VMs')) + class PoolInfo(BaseComponentInfo): ''' @@ -203,3 +267,6 @@ class PoolInfo(BaseComponentInfo): oc_memory_ratio_hvm = ProxyProperty('oc_memory_ratio_hvm') oc_memory_ratio_pv = ProxyProperty('oc_memory_ratio_pv') vswitch_controller = ProxyProperty('vswitch_controller') + + host_count = RelationshipLengthProperty('hosts') + vm_count = RelationshipLengthProperty('vms') diff --git a/ZenPacks/zenoss/XenServer/SR.py b/ZenPacks/zenoss/XenServer/SR.py index c255d54..375123d 100644 --- a/ZenPacks/zenoss/XenServer/SR.py +++ b/ZenPacks/zenoss/XenServer/SR.py @@ -21,6 +21,7 @@ PooledComponent, IPooledComponentInfo, PooledComponentInfo, RelationshipLengthProperty, updateToMany, + id_from_ref, ids_from_refs, ) @@ -65,6 +66,39 @@ class SR(PooledComponent): ('local_cache_for_hosts', ToMany(ToOne, MODULE_NAME['Host'], 'local_cache_sr')), ) + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI SR ref and properties. + ''' + if 'uuid' not in properties: + return { + 'relname': 'sr', + 'id': id_from_ref(ref), + } + + title = properties.get('name_label') or properties['uuid'] + + sm_config = properties.get('sm_config', {}) + + return { + 'relname': 'srs', + 'id': id_from_ref(ref), + 'title': title, + 'xenapi_ref': ref, + 'xenapi_uuid': properties.get('uuid'), + 'allowed_operations': properties.get('allowed_operations'), + 'content_type': properties.get('content_type'), + 'local_cache_enabled': properties.get('local_cache_enabled'), + 'name_description': properties.get('name_description'), + 'name_label': properties.get('name_label'), + 'physical_size': properties.get('physical_size'), + 'shared': properties.get('shared'), + 'sm_type': sm_config.get('type'), + 'sr_type': properties.get('type'), + 'setPBDs': ids_from_refs(properties.get('PBDs', [])) + } + def getPBDs(self): ''' Return a sorted list of related PBD ids. @@ -212,8 +246,14 @@ def xenrrd_prefix(self): ''' # This is a guess at future support. XenServer 6.2 doesn't have # any RRD data for SRs. - if self.xapi_uuid: - return ('sr', self.xapi_uuid, '') + if self.xenapi_uuid: + return ('sr', self.xenapi_uuid, '') + + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/storage-domain.png' class ISRInfo(IPooledComponentInfo): diff --git a/ZenPacks/zenoss/XenServer/VBD.py b/ZenPacks/zenoss/XenServer/VBD.py index 33ae49f..54e80f9 100644 --- a/ZenPacks/zenoss/XenServer/VBD.py +++ b/ZenPacks/zenoss/XenServer/VBD.py @@ -22,6 +22,7 @@ PooledComponent, IPooledComponentInfo, PooledComponentInfo, RelationshipInfoProperty, updateToOne, + id_from_ref, ) @@ -32,7 +33,7 @@ class VBD(PooledComponent): meta_type = portal_type = 'XenServerVBD' - xapi_metrics_ref = None + xenapi_metrics_ref = None allowed_operations = None bootable = None currently_attached = None @@ -45,7 +46,7 @@ class VBD(PooledComponent): userdevice = None _properties = PooledComponent._properties + ( - {'id': 'xapi_metrics_ref', 'type': 'string', 'mode': 'w'}, + {'id': 'xenapi_metrics_ref', 'type': 'string', 'mode': 'w'}, {'id': 'allowed_operations', 'type': 'lines', 'mode': 'w'}, {'id': 'bootable', 'type': 'boolean', 'mode': 'w'}, {'id': 'currently_attached', 'type': 'boolean', 'mode': 'w'}, @@ -63,6 +64,43 @@ class VBD(PooledComponent): ('vdi', ToOne(ToMany, MODULE_NAME['VDI'], 'vbds')), ) + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI VBD ref and properties. + ''' + if 'uuid' not in properties: + return { + 'compname': 'vms/{}'.format(id_from_ref(properties['parent'])), + 'relname': 'vbds', + 'id': id_from_ref(ref), + } + + title = properties.get('device') or \ + properties.get('userdevice') or \ + properties['uuid'] + + return { + 'compname': 'vms/{}'.format(id_from_ref(properties.get('VM'))), + 'relname': 'vbds', + 'id': id_from_ref(ref), + 'title': title, + 'xenapi_ref': ref, + 'xenapi_metrics_ref': properties.get('metrics'), + 'xenapi_uuid': properties.get('uuid'), + 'allowed_operations': properties.get('allowed_operations'), + 'bootable': properties.get('bootable'), + 'currently_attached': properties.get('currently_attached'), + 'vbd_device': properties.get('device'), + 'empty': properties.get('empty'), + 'mode': properties.get('mode'), + 'storage_lock': properties.get('storage_lock'), + 'vbd_type': properties.get('type'), + 'unpluggable': properties.get('unpluggable'), + 'userdevice': properties.get('userdevice'), + 'setVDI': id_from_ref(properties.get('VDI')), + } + def getVDI(self): ''' Return VDI id or None. @@ -90,10 +128,27 @@ def xenrrd_prefix(self): Return prefix under which XenServer stores RRD data about this component. ''' - vm_uuid = self.vm().xapi_uuid + vm_uuid = self.vm().xenapi_uuid if vm_uuid and self.vbd_device: return ('vm', vm_uuid, '_'.join(('vbd', self.vbd_device))) + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/virtual-disk.png' + + def guest_disk(self): + ''' + Return the guest disk associated with this VBD. + ''' + if not self.vbd_device: + return + + guest_device = self.vm().guest_device() + if guest_device: + return guest_device.hw.harddisks._getOb(self.vbd_device, None) + class IVBDInfo(IPooledComponentInfo): ''' @@ -102,6 +157,7 @@ class IVBDInfo(IPooledComponentInfo): vm = schema.Entity(title=_t(u'VM')) vdi = schema.Entity(title=_t(u'VDI')) + guest_disk = schema.Entity(title=_t(u'Guest Disk')) allowed_operations = schema.TextLine(title=_t(u'Allowed Operations')) bootable = schema.TextLine(title=_t(u'Bootable')) @@ -125,8 +181,9 @@ class VBDInfo(PooledComponentInfo): vm = RelationshipInfoProperty('vm') vdi = RelationshipInfoProperty('vdi') + guest_disk = RelationshipInfoProperty('guest_disk') - xapi_metrics_ref = ProxyProperty('xapi_metrics_ref') + xenapi_metrics_ref = ProxyProperty('xenapi_metrics_ref') allowed_operations = ProxyProperty('allowed_operations') bootable = ProxyProperty('bootable') currently_attached = ProxyProperty('currently_attached') diff --git a/ZenPacks/zenoss/XenServer/VDI.py b/ZenPacks/zenoss/XenServer/VDI.py index 5fc4249..9acfbe7 100644 --- a/ZenPacks/zenoss/XenServer/VDI.py +++ b/ZenPacks/zenoss/XenServer/VDI.py @@ -21,6 +21,7 @@ PooledComponent, IPooledComponentInfo, PooledComponentInfo, RelationshipInfoProperty, RelationshipLengthProperty, updateToMany, + id_from_ref, ids_from_refs, ) @@ -67,6 +68,46 @@ class VDI(PooledComponent): ('vbds', ToMany(ToOne, MODULE_NAME['VBD'], 'vdi')), ) + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI VDI ref and properties. + ''' + if 'uuid' not in properties: + return { + 'compname': 'srs/{}'.format(id_from_ref(properties['parent'])), + 'relname': 'vdis', + 'id': id_from_ref(ref), + } + + title = properties.get('name_label') or \ + properties.get('location') or \ + properties['uuid'] + + return { + 'compname': 'srs/{}'.format(id_from_ref(properties.get('SR'))), + 'relname': 'vdis', + 'id': id_from_ref(ref), + 'title': title, + 'xenapi_ref': ref, + 'xenapi_uuid': properties.get('uuid'), + 'allow_caching': properties.get('allow_caching'), + 'allowed_operations': properties.get('allowed_operations'), + 'is_a_snapshot': properties.get('is_a_snapshot'), + 'location': properties.get('location'), + 'managed': properties.get('managed'), + 'missing': properties.get('missing'), + 'name_description': properties.get('name_description'), + 'name_label': properties.get('name_label'), + 'on_boot': properties.get('on_boot'), + 'read_only': properties.get('read_only'), + 'sharable': properties.get('sharable'), + 'storage_lock': properties.get('storage_lock'), + 'vdi_type': properties.get('type'), + 'virtual_size': properties.get('virtual_size'), + 'setVBDs': ids_from_refs(properties.get('VBDs', [])), + } + def getVBDs(self): ''' Return a sorted list of ids in the vbds relationship. @@ -95,8 +136,14 @@ def xenrrd_prefix(self): ''' # This is a guess at future support. XenServer 6.2 doesn't have # any RRD data for VDIs. - if self.xapi_uuid: - return ('vdi', self.xapi_uuid, '') + if self.xenapi_uuid: + return ('vdi', self.xenapi_uuid, '') + + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/virtual-disk.png' class IVDIInfo(IPooledComponentInfo): diff --git a/ZenPacks/zenoss/XenServer/VIF.py b/ZenPacks/zenoss/XenServer/VIF.py index 9224fdf..dde7ebf 100644 --- a/ZenPacks/zenoss/XenServer/VIF.py +++ b/ZenPacks/zenoss/XenServer/VIF.py @@ -8,10 +8,14 @@ # ###################################################################### +import logging +LOG = logging.getLogger('xen.XenServer') + from zope.component import adapts from zope.interface import implements from Products.ZenRelations.RelSchema import ToMany, ToManyCont, ToOne +from Products.ZenUtils.Utils import prepId from Products.Zuul.catalog.paths import DefaultPathReporter, relPath from Products.Zuul.form import schema from Products.Zuul.infos import ProxyProperty @@ -22,6 +26,7 @@ PooledComponent, IPooledComponentInfo, PooledComponentInfo, RelationshipInfoProperty, updateToOne, + id_from_ref, ) @@ -32,7 +37,7 @@ class VIF(PooledComponent): meta_type = portal_type = 'XenServerVIF' - xapi_metrics_ref = None + xenapi_metrics_ref = None macaddress = None mac_autogenerated = None mtu = None @@ -44,7 +49,7 @@ class VIF(PooledComponent): locking_mode = None _properties = PooledComponent._properties + ( - {'id': 'xapi_metrics_ref', 'type': 'string', 'mode': 'w'}, + {'id': 'xenapi_metrics_ref', 'type': 'string', 'mode': 'w'}, {'id': 'macaddress', 'type': 'string', 'mode': 'w'}, {'id': 'mac_autogenerated', 'type': 'boolean', 'mode': 'w'}, {'id': 'mtu', 'type': 'string', 'mode': 'w'}, @@ -61,6 +66,74 @@ class VIF(PooledComponent): ('network', ToOne(ToMany, MODULE_NAME['Network'], 'vifs')), ) + _catalogs = dict({ + 'VIFCatalog': { + 'deviceclass': '/XenServer', + 'indexes': { + 'ipv4_addresses': {'type': 'keyword'}, + 'mac_addresses': {'type': 'keyword'}, + }, + }, + }, **PooledComponent._catalogs) + + @property + def ipv4_addresses(self): + return self.ipv4_allowed + + @property + def mac_addresses(self): + return (self.macaddress,) + + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI VIF ref and properties. + ''' + if 'uuid' not in properties: + return { + 'compname': 'vms/{}'.format(id_from_ref(properties['parent'])), + 'relname': 'vifs', + 'id': id_from_ref(ref), + } + + title = properties.get('device') or properties['uuid'] + + return { + 'compname': 'vms/{}'.format(id_from_ref(properties.get('VM'))), + 'relname': 'vifs', + 'id': id_from_ref(ref), + 'title': title, + 'xenapi_ref': ref, + 'xenapi_metrics_ref': properties.get('metrics'), + 'xenapi_uuid': properties.get('uuid'), + 'macaddress': properties.get('MAC'), + 'mac_autogenerated': properties.get('MAC_autogenerated'), + 'mtu': properties.get('MTU'), + 'allowed_operations': properties.get('allowed_operations'), + 'currently_attached': properties.get('currently_attached'), + 'vif_device': properties.get('device'), + 'ipv4_allowed': properties.get('ipv4_allowed'), + 'ipv6_allowed': properties.get('ipv6_allowed'), + 'locking_mode': properties.get('locking_mode'), + 'setNetwork': id_from_ref(properties.get('network')), + } + + @classmethod + def findByIP(cls, dmd, ipv4_addresses): + ''' + Return the first VIF matching one of ipv4_addresses. + ''' + return next(cls.search( + dmd, 'VIFCatalog', ipv4_addresses=ipv4_addresses), None) + + @classmethod + def findByMAC(cls, dmd, mac_addresses): + ''' + Return the first VIF matching one of mac_addresses. + ''' + return next(cls.search( + dmd, 'VIFCatalog', mac_addresses=mac_addresses), None) + def getNetwork(self): ''' Return network id or None. @@ -88,10 +161,28 @@ def xenrrd_prefix(self): Return prefix under which XenServer stores RRD data about this component. ''' - vm_uuid = self.vm().xapi_uuid + vm_uuid = self.vm().xenapi_uuid if vm_uuid and self.vif_device: return ('vm', vm_uuid, '_'.join(('vif', self.vif_device))) + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/virtual-network-interface.png' + + def guest_interface(self): + ''' + Return the guest interface associated with this VIF. + ''' + if not self.vif_device: + return + + guest_device = self.vm().guest_device() + if guest_device: + return guest_device.os.interfaces._getOb( + prepId(self.vif_device), None) + class IVIFInfo(IPooledComponentInfo): ''' @@ -100,6 +191,7 @@ class IVIFInfo(IPooledComponentInfo): vm = schema.Entity(title=_t(u'VM')) network = schema.Entity(title=_t(u'Network')) + guest_interface = schema.Entity(title=_t(u'Guest Interface')) macaddress = schema.TextLine(title=_t(u'MAC Address')) mac_autogenerated = schema.TextLine(title=_t(u'Autogenerate MAC Address')) @@ -122,8 +214,9 @@ class VIFInfo(PooledComponentInfo): vm = RelationshipInfoProperty('vm') network = RelationshipInfoProperty('network') + guest_interface = RelationshipInfoProperty('guest_interface') - xapi_metrics_ref = ProxyProperty('xapi_metrics_ref') + xenapi_metrics_ref = ProxyProperty('xenapi_metrics_ref') macaddress = ProxyProperty('macaddress') mac_autogenerated = ProxyProperty('mac_autogenerated') mtu = ProxyProperty('mtu') diff --git a/ZenPacks/zenoss/XenServer/VM.py b/ZenPacks/zenoss/XenServer/VM.py index cc95dfa..e821a48 100644 --- a/ZenPacks/zenoss/XenServer/VM.py +++ b/ZenPacks/zenoss/XenServer/VM.py @@ -8,6 +8,8 @@ # ###################################################################### +import itertools + from zope.component import adapts from zope.interface import implements @@ -22,6 +24,9 @@ PooledComponent, IPooledComponentInfo, PooledComponentInfo, RelationshipInfoProperty, RelationshipLengthProperty, updateToOne, + id_from_ref, int_or_none, + findIpInterfacesByMAC, + require_zenpack, ) @@ -32,8 +37,8 @@ class VM(PooledComponent): meta_type = portal_type = 'XenServerVM' - xapi_metrics_ref = None - xapi_guest_metrics_ref = None + xenapi_metrics_ref = None + xenapi_guest_metrics_ref = None hvm_shadow_multiplier = None vcpus_at_startup = None vcpus_max = None @@ -59,8 +64,8 @@ class VM(PooledComponent): version = None _properties = PooledComponent._properties + ( - {'id': 'xapi_metrics_ref', 'type': 'string', 'mode': 'w'}, - {'id': 'xapi_guest_metrics_ref', 'type': 'string', 'mode': 'w'}, + {'id': 'xenapi_metrics_ref', 'type': 'string', 'mode': 'w'}, + {'id': 'xenapi_guest_metrics_ref', 'type': 'string', 'mode': 'w'}, {'id': 'hvm_shadow_multiplier', 'type': 'float', 'mode': 'w'}, {'id': 'vcpus_at_startup', 'type': 'int', 'mode': 'w'}, {'id': 'vcpus_max', 'type': 'int', 'mode': 'w'}, @@ -94,6 +99,84 @@ class VM(PooledComponent): ('vmappliance', ToOne(ToMany, MODULE_NAME['VMAppliance'], 'vms')), ) + @property + def ipv4_addresses(self): + return tuple(itertools.chain.from_iterable( + x.ipv4_allowed for x in self.vifs() if x.ipv4_allowed)) + + @property + def mac_addresses(self): + return tuple(x.macaddress for x in self.vifs() if x.macaddress) + + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI VM ref and properties. + ''' + if not properties: + return { + 'relname': 'vms', + 'id': id_from_ref(ref), + } + + if properties.get('is_a_snapshot') or \ + properties.get('is_snapshot_from_vmpp') or \ + properties.get('is_a_template'): + + return + + title = properties.get('name_label') or properties['uuid'] + + guest_metrics_ref = properties.get('guest_metrics') + if guest_metrics_ref == 'OpaqueRef:NULL': + guest_metrics_ref = None + + return { + 'relname': 'vms', + 'id': id_from_ref(ref), + 'title': title, + 'xenapi_ref': ref, + 'xenapi_metrics_ref': properties.get('metrics'), + 'xenapi_guest_metrics_ref': guest_metrics_ref, + 'xenapi_uuid': properties.get('uuid'), + 'hvm_shadow_multiplier': properties.get('HVM_shadow_multiplier'), + 'vcpus_at_startup': int_or_none(properties.get('VCPUs_at_startup')), + 'vcpus_max': int_or_none(properties.get('VCPUs_max')), + 'actions_after_crash': properties.get('actions_after_crash'), + 'actions_after_reboot': properties.get('actions_after_reboot'), + 'actions_after_shutdown': properties.get('actions_after_shutdown'), + 'allowed_operations': properties.get('allowed_operations'), + 'domarch': properties.get('domarch'), + 'domid': int_or_none(properties.get('domid')), + 'ha_always_run': properties.get('ha_always_run'), + 'ha_restart_priority': properties.get('ha_restart_priority'), + 'is_a_snapshot': properties.get('is_a_snapshot'), + 'is_a_template': properties.get('is_a_template'), + 'is_control_domain': properties.get('is_control_domain'), + 'is_snapshot_from_vmpp': properties.get('is_snapshot_from_vmpp'), + 'name_description': properties.get('name_description'), + 'name_label': properties.get('name_label'), + 'power_state': properties.get('power_state'), + 'shutdown_delay': int_or_none(properties.get('shutdown_delay')), + 'start_delay': int_or_none(properties.get('start_delay')), + 'user_version': int_or_none(properties.get('user_version')), + 'version': int_or_none(properties.get('version')), + 'setHost': id_from_ref(properties.get('resident_on')), + 'setVMAppliance': id_from_ref(properties.get('appliance')), + } + + @classmethod + def objectmap_metrics(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI host ref and host_metrics + properties. + ''' + return { + 'relname': 'vms', + 'id': id_from_ref(ref), + 'memory_actual': int_or_none(properties.get('memory_actual')), + } + def getHost(self): ''' Return host id or None. @@ -145,7 +228,7 @@ def getRRDTemplates(self): template_names = [self.getRRDTemplateName()] # Bind the guest template only if guest metrics are available. - if self.xapi_guest_metrics_ref: + if self.xenapi_guest_metrics_ref: template_names.append('{0}Guest'.format(self.getRRDTemplateName())) templates = [] @@ -161,8 +244,65 @@ def xenrrd_prefix(self): Return prefix under which XenServer stores RRD data about this component. ''' - if self.xapi_uuid: - return ('vm', self.xapi_uuid, '') + if self.xenapi_uuid: + return ('vm', self.xenapi_uuid, '') + + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/virtual-server.png' + + def guest_device(self): + ''' + Return the guest device running in the VM. + ''' + macaddresses = [x.macaddress for x in self.vifs() if x.macaddress] + if macaddresses: + for iface in findIpInterfacesByMAC(self.dmd, macaddresses): + return iface.device() + + @require_zenpack('ZenPacks.zenoss.CloudStack') + def cloudstack_routervm(self): + ''' + Return the associated CloudStack router VM. + ''' + from ZenPacks.zenoss.CloudStack.RouterVM import RouterVM + + try: + return RouterVM.findByMAC(self.dmd, self.mac_addresses) + except AttributeError: + # The CloudStack RouterVM class didn't gain the findByMAC + # method until version 1.1 of the ZenPack. + pass + + @require_zenpack('ZenPacks.zenoss.CloudStack') + def cloudstack_systemvm(self): + ''' + Return the associated CloudStack system VM. + ''' + from ZenPacks.zenoss.CloudStack.SystemVM import SystemVM + + try: + return SystemVM.findByMAC(self.dmd, self.mac_addresses) + except AttributeError: + # The CloudStack SystemVM class didn't gain the findByMAC + # method until version 1.1 of the ZenPack. + pass + + @require_zenpack('ZenPacks.zenoss.CloudStack') + def cloudstack_vm(self): + ''' + Return the associated CloudStack VirtualMachine. + ''' + from ZenPacks.zenoss.CloudStack.VirtualMachine import VirtualMachine + + try: + return VirtualMachine.findByMAC(self.dmd, self.mac_addresses) + except AttributeError: + # The CloudStack VirtualMachine class didn't gain the + # findByMAC method until version 1.1 of the ZenPack. + pass class IVMInfo(IPooledComponentInfo): @@ -172,6 +312,7 @@ class IVMInfo(IPooledComponentInfo): host = schema.Entity(title=_t(u'Host')) vmappliance = schema.Entity(title=_t(u'vApp')) + guest_device = schema.Entity(title=_t(u'Guest Device')) hvm_shadow_multiplier = schema.Float(title=_t(u'HVM Shadow Multiplier')) vcpus_at_startup = schema.Int(title=_t(u'vCPUs at Startup')) @@ -190,7 +331,7 @@ class IVMInfo(IPooledComponentInfo): is_snapshot_from_vmpp = schema.Bool(title=_t(u'Is a Snapshot from VMPP')) memory_actual = schema.Int(title=_t(u'Actual Memory')) name_description = schema.TextLine(title=_t(u'Description')) - name_label = schema.TextLine(title=_t(u'Name')) + name_label = schema.TextLine(title=_t(u'Label')) power_state = schema.TextLine(title=_t(u'Power State')) shutdown_delay = schema.Int(title=_t(u'Shutdown Delay')) start_delay = schema.Int(title=_t(u'Start Delay')) @@ -211,9 +352,10 @@ class VMInfo(PooledComponentInfo): host = RelationshipInfoProperty('host') vmappliance = RelationshipInfoProperty('vmappliance') + guest_device = RelationshipInfoProperty('guest_device') - xapi_metrics_ref = ProxyProperty('xapi_metrics_ref') - xapi_guest_metrics_ref = ProxyProperty('xapi_guest_metrics_ref') + xenapi_metrics_ref = ProxyProperty('xenapi_metrics_ref') + xenapi_guest_metrics_ref = ProxyProperty('xenapi_guest_metrics_ref') hvm_shadow_multiplier = ProxyProperty('hvm_shadow_multiplier') vcpus_at_startup = ProxyProperty('vcpus_at_startup') vcpus_max = ProxyProperty('vcpus_max') diff --git a/ZenPacks/zenoss/XenServer/VMAppliance.py b/ZenPacks/zenoss/XenServer/VMAppliance.py index 61a28fb..c3f6067 100644 --- a/ZenPacks/zenoss/XenServer/VMAppliance.py +++ b/ZenPacks/zenoss/XenServer/VMAppliance.py @@ -21,6 +21,7 @@ PooledComponent, IPooledComponentInfo, PooledComponentInfo, RelationshipLengthProperty, updateToMany, + id_from_ref, ids_from_refs, ) @@ -42,10 +43,36 @@ class VMAppliance(PooledComponent): ) _relations = PooledComponent._relations + ( - ('endpoint', ToOne(ToManyCont, MODULE_NAME['Endpoint'], 'vmappliances',)), + ('endpoint', ToOne(ToManyCont, MODULE_NAME['Endpoint'], 'vmappliances')), ('vms', ToMany(ToOne, MODULE_NAME['VM'], 'vmappliance')), ) + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI vm_appliance ref and + properties. + ''' + if not properties: + return { + 'relname': 'vmappliances', + 'id': id_from_ref(ref), + } + + title = properties.get('name_label') or properties['uuid'] + + return { + 'relname': 'vmappliances', + 'id': id_from_ref(ref), + 'title': title, + 'xenapi_ref': ref, + 'xenapi_uuid': properties.get('uuid'), + 'allowed_operations': properties.get('allowed_operations'), + 'name_description': properties.get('name_description'), + 'name_label': properties.get('name_label'), + 'setVMs': ids_from_refs(properties.get('VMs', [])), + } + def getVMs(self): ''' Return a sorted list of related VM ids. @@ -74,8 +101,14 @@ def xenrrd_prefix(self): ''' # This is a guess at future support. XenServer 6.2 doesn't have # any RRD data for VMAppliances. - if self.xapi_uuid: - return ('vmappliance', self.xapi_uuid, '') + if self.xenapi_uuid: + return ('vmappliance', self.xenapi_uuid, '') + + def getIconPath(self): + ''' + Return URL to icon representing objects of this class. + ''' + return '/++resource++xenserver/img/virtual-server.png' class IVMApplianceInfo(IPooledComponentInfo): @@ -85,7 +118,7 @@ class IVMApplianceInfo(IPooledComponentInfo): allowed_operations = schema.Text(title=_t(u'Allowed Operations')) name_description = schema.TextLine(title=_t(u'Description')) - name_label = schema.TextLine(title=_t(u'Name')) + name_label = schema.TextLine(title=_t(u'Label')) vm_count = schema.Int(title=_t(u'Number of VMs')) diff --git a/ZenPacks/zenoss/XenServer/__init__.py b/ZenPacks/zenoss/XenServer/__init__.py index 311cba7..01e9c9a 100644 --- a/ZenPacks/zenoss/XenServer/__init__.py +++ b/ZenPacks/zenoss/XenServer/__init__.py @@ -11,23 +11,14 @@ import logging LOG = logging.getLogger('zen.XenServer') -import os import Globals -from Products.ZenModel.Device import Device from Products.ZenModel.ZenPack import ZenPack as ZenPackBase from Products.ZenRelations.zPropertyCategory import setzPropertyCategory -from Products.ZenRelations.RelSchema import ToManyCont, ToOne -from Products.CMFCore.DirectoryView import registerDirectory -from Products.Zuul.interfaces import ICatalogTool -from Products.ZenUtils.Utils import unused, zenPath +from Products.ZenUtils.Utils import unused unused(Globals) -skinsDir = os.path.join(os.path.dirname(__file__), 'skins') -if os.path.isdir(skinsDir): - registerDirectory(skinsDir, globals()) - ZENPACK_NAME = 'ZenPacks.zenoss.XenServer' # Modules containing model classes. Used by zenchkschema to validate @@ -48,23 +39,6 @@ 'VMAppliance', ) -# Define new device relations. -NEW_DEVICE_RELATIONS = ( - ) - -NEW_COMPONENT_TYPES = ( - ) - -# Add new relationships to Device if they don't already exist. -for relname, modname in NEW_DEVICE_RELATIONS: - if relname not in (x[0] for x in Device._relations): - Device._relations += ( - (relname, ToManyCont( - ToOne, - '.'.join((ZENPACK_NAME, modname)), - '%s_host' % modname)), - ) - # Useful to avoid making literal string references to module and class names # throughout the rest of the ZenPack. MODULE_NAME = {} @@ -74,78 +48,36 @@ MODULE_NAME[product_name] = '.'.join([ZENPACK_NAME, product_name]) CLASS_NAME[product_name] = '.'.join([ZENPACK_NAME, product_name, product_name]) -_PACK_Z_PROPS = [ - ('zXenServerAddresses', [], 'lines'), - ('zXenServerUsername', 'root', 'string'), - ('zXenServerPassword', '', 'password'), - ('zXenServerCollectionInterval', 300, 'int'), - ] - setzPropertyCategory('zXenServerAddresses', 'XenServer') setzPropertyCategory('zXenServerUsername', 'XenServer') setzPropertyCategory('zXenServerPassword', 'XenServer') -setzPropertyCategory('zXenServerCollectionInterval', 'XenServer') - -_plugins = ( - ) +setzPropertyCategory('zXenServerPerfInterval', 'XenServer') +setzPropertyCategory('zXenServerModelInterval', 'XenServer') +setzPropertyCategory('zXenServerEventsInterval', 'XenServer') class ZenPack(ZenPackBase): - packZProperties = _PACK_Z_PROPS + packZProperties = [ + ('zXenServerAddresses', [], 'lines'), + ('zXenServerUsername', 'root', 'string'), + ('zXenServerPassword', '', 'password'), + ('zXenServerPerfInterval', 300, 'int'), + ('zXenServerModelInterval', 60, 'int'), + ('zXenServerEventsInterval', 60, 'int'), + ] def install(self, app): - super(ZenPack, self).install(app) - - if NEW_DEVICE_RELATIONS: - LOG.info('Adding ZenPacks.zenoss.XenServer relationships to existing devices') - self._buildDeviceRelations() + ZenPackBase.install(self, app) - if _plugins: - self.symlink_plugins() + self.installBinFile('zenxenservermodeler') def remove(self, app, leaveObjects=False): if not leaveObjects: - if _plugins: - self.remove_plugin_symlinks() - - if NEW_COMPONENT_TYPES: - LOG.info('Removing ZenPacks.zenoss.XenServer components') - - # Search the catalog for components of this zenpacks type. - cat = ICatalogTool(app.zport.dmd) - - for brain in cat.search(types=NEW_COMPONENT_TYPES): - component = brain.getObject() - component.getPrimaryParent()._delObject(component.id) - - # Remove our Device relations additions. - if NEW_DEVICE_RELATIONS: - Device._relations = tuple( - [x for x in Device._relations - if x[0] not in NEW_DEVICE_RELATIONS]) - - LOG.info('Removing ZenPacks.zenoss.XenServer relationships from existing devices') - self._buildDeviceRelations() - - super(ZenPack, self).remove(app, leaveObjects=leaveObjects) - - def symlink_plugins(self): - libexec = os.path.join(os.environ.get('ZENHOME'), 'libexec') - if not os.path.isdir(libexec): - # Stack installs might not have a $ZENHOME/libexec directory. - os.mkdir(libexec) - - for plugin in _plugins: - LOG.info('Linking %s plugin into $ZENHOME/libexec/', plugin) - plugin_path = zenPath('libexec', plugin) - os.system('ln -sf "%s" "%s"' % (self.path(plugin), plugin_path)) - os.system('chmod 0755 %s' % plugin_path) - - def remove_plugin_symlinks(self): - for plugin in _plugins: - LOG.info('Removing %s link from $ZENHOME/libexec/', plugin) - os.system('rm -f "%s"' % zenPath('libexec', plugin)) - - def _buildDeviceRelations(self): - for d in self.dmd.Devices.getSubDevicesGen(): - d.buildRelations() + self.removeBinFile('zenxenservermodeler') + + ZenPackBase.remove(self, app, leaveObjects=leaveObjects) + + +# Patch last to avoid import recursion problems. +from ZenPacks.zenoss.XenServer import patches +unused(patches) diff --git a/ZenPacks/zenoss/XenServer/api.py b/ZenPacks/zenoss/XenServer/api.py index 9c8a712..3b98a08 100644 --- a/ZenPacks/zenoss/XenServer/api.py +++ b/ZenPacks/zenoss/XenServer/api.py @@ -11,21 +11,15 @@ API interfaces and default implementations. ''' -import time - -from zope.event import notify from zope.interface import implements -from ZODB.transact import transact - from Products.ZenUtils.Ext import DirectRouter, DirectResponse from Products import Zuul -from Products.ZenUtils.Utils import prepId -from Products.Zuul.catalog.events import IndexingEvent +from Products.ZenModel.ZDeviceLoader import DeviceCreationJob +from Products.ZenUtils.Utils import binPath from Products.Zuul.facades import ZuulFacade from Products.Zuul.interfaces import IFacade -from Products.Zuul.utils import ZuulMessageFactory as _t class IXenServerFacade(IFacade): @@ -47,39 +41,52 @@ class XenServerFacade(ZuulFacade): implements(IXenServerFacade) def add_xenserver(self, name, address, username, password, collector='localhost'): - device_id = prepId(name) - devices = self._dmd.getDmdRoot('Devices') - device = devices.findDeviceByIdExact(device_id) - if device: - return False, _t("A resource named %s already exists." % name) - - @transact - def create_device(): - device_class = self._dmd.Devices.getOrganizer('/XenServer') - - endpoint = device_class.createInstance(device_id) - endpoint.title = name - endpoint.setPerformanceMonitor(collector) - endpoint.setZenProperty('zXenServerAddresses', [address]) - endpoint.setZenProperty('zXenServerUsername', username) - endpoint.setZenProperty('zXenServerPassword', password) - endpoint.index_object() - notify(IndexingEvent(endpoint)) - - # This must be committed before the following model can be - # scheduled. - create_device() - - # TODO: Fix this. - # Sleep to make sure zenhub is ready to service the modeling job - # when we run collectDevice below. - time.sleep(5) - - # Schedule a modeling job for the new device. - endpoint = devices.findDeviceByIdExact(device_id) - endpoint.collectDevice(setlog=False, background=True) - - return True + zProps = { + 'zXenServerAddresses': [address], + 'zXenServerUsername': username, + 'zXenServerPassword': password, + } + + zendiscCmd = [ + binPath('zenxenservermodeler'), + 'run', '--now', + '-d', name, + '--monitor', collector, + ] + + kwargs = { + 'deviceName': name, + 'devicePath': '/XenServer', + 'title': name, + 'discoverProto': 'XenAPI', + 'manageIp': '', + 'performanceMonitor': collector, + 'rackSlot': 0, + 'productionState': 1000, + 'comments': '', + 'hwManufacturer': '', + 'hwProductName': '', + 'osManufacturer': '', + 'osProductName': '', + 'priority': 3, + 'tag': '', + 'serialNumber': '', + 'locationPath': '', + 'systemPaths': [], + 'groupPaths': [], + 'zProperties': zProps, + 'zendiscCmd': zendiscCmd, + } + + try: + job_status = self._dmd.JobManager.addJob( + DeviceCreationJob, kwargs=kwargs) + except TypeError: + # 4.1.1 compatibility. + job_status = self._dmd.JobManager.addJob( + DeviceCreationJob, **kwargs) + + return job_status class XenServerRouter(DirectRouter): diff --git a/ZenPacks/zenoss/XenServer/bin/zenxenservermodeler b/ZenPacks/zenoss/XenServer/bin/zenxenservermodeler new file mode 100755 index 0000000..2a9d7c7 --- /dev/null +++ b/ZenPacks/zenoss/XenServer/bin/zenxenservermodeler @@ -0,0 +1,43 @@ +#! /usr/bin/env bash +############################################################################## +# +# Copyright (C) Zenoss, Inc. 2013, all rights reserved. +# +# This content is made available according to terms specified in +# License.zenoss under the directory where your Zenoss product is installed. +# +############################################################################## + +. $ZENHOME/bin/zenfunctions + +logmsg() { + date=`date '+%Y-%m-%d %H:%M:%S,%N' | cut -c-23` + echo "$date INFO zen.XenServer: $@" +} + +# zenxenservermodeler is just a wrapper around zenmodeler, so that it can be +# invoked by DeviceCreationJob. However, since that script expects to be +# calling zendisc, it includes a mandatory commandline argument, --job, which +# zenmodeler will not accept. + +args=($@) + +for((i=0; i<${#args[@]}; i++)); do + + # Strip --job commandline argument and its value + if [ "${args[$i]}" = "--job" ]; then + unset args[$i] + unset args[$i+1] + fi +done + +# Allow time for zenhub to realize that the device exists (which could take up +# to 30 seconds) (TODO: find a real fix for this problem- the same issue comes +# up in zendisc.discoverDevice(), where it calls getJobProperties too soon to +# get any data) + +logmsg "Waiting For ZenHub.." +sleep 40 + +logmsg "Executing: $ZENHOME/bin/zenmodeler run ${args[@]}" +$ZENHOME/bin/zenmodeler run "${args[@]}" diff --git a/ZenPacks/zenoss/XenServer/configure.zcml b/ZenPacks/zenoss/XenServer/configure.zcml index 29d6696..472ce0f 100644 --- a/ZenPacks/zenoss/XenServer/configure.zcml +++ b/ZenPacks/zenoss/XenServer/configure.zcml @@ -2,6 +2,7 @@ @@ -25,15 +26,15 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ZenPacks/zenoss/XenServer/datasource_plugins.py b/ZenPacks/zenoss/XenServer/datasource_plugins.py index 6ada8e1..c5dcb50 100644 --- a/ZenPacks/zenoss/XenServer/datasource_plugins.py +++ b/ZenPacks/zenoss/XenServer/datasource_plugins.py @@ -13,9 +13,11 @@ import collections import math import re +import time from cStringIO import StringIO from lxml import etree +from xmlrpclib import DateTime from twisted.internet.defer import inlineCallbacks, returnValue @@ -24,6 +26,7 @@ from ZenPacks.zenoss.PythonCollector.datasources.PythonDataSource import \ PythonDataSourcePlugin +from ZenPacks.zenoss.XenServer.modeler.incremental import DataMapProducer from ZenPacks.zenoss.XenServer.utils import add_local_lib_path # Allows txxenapi to be imported. @@ -66,42 +69,69 @@ def get_event(config, message, severity): } -class XenServerXAPIDataSourcePlugin(PythonDataSourcePlugin): +def aggregate_values(datapoint, columns): + ''' + Return column values aggregated according to datapoint configuration. + ''' + aggregate = { + 'AVERAGE': lambda x: sum(x) / len(x), + 'MAX': max, + 'MIN': min, + 'SUM': sum, + } + + return aggregate[datapoint.group_aggregation]([ + aggregate[datapoint.time_aggregation](x) for x in columns]) + + +class BasePlugin(PythonDataSourcePlugin): + ''' + Abstract base class for functionality common to XenServer datasource + plugins. + ''' + proxy_attributes = [ 'xenserver_addresses', 'zXenServerUsername', 'zXenServerPassword', ] + def collect(self, config): + ds0 = config.datasources[0] + + client = get_client( + ds0.xenserver_addresses, + ds0.zXenServerUsername, + ds0.zXenServerPassword) + + return self.collect_xen(config, ds0, client) + + +class XenAPIPlugin(BasePlugin): + ''' + Collects XenServer XenAPI datasources. + ''' + @classmethod def config_key(cls, datasource, context): - return ( - context.device().id, - datasource.getCycleTime(context), - datasource.xapi_classname, + return BasePlugin.config_key(datasource, context) + ( + datasource.xenapi_classname, ) @classmethod def params(cls, datasource, context): return { - 'xapi_classname': datasource.talesEval(datasource.xapi_classname, context), - 'xapi_ref': datasource.talesEval(datasource.xapi_ref, context), + 'xenapi_classname': datasource.talesEval(datasource.xenapi_classname, context), + 'xenapi_ref': datasource.talesEval(datasource.xenapi_ref, context), } - def collect(self, config): - ds0 = config.datasources[0] - - client = get_client( - ds0.xenserver_addresses, - ds0.zXenServerUsername, - ds0.zXenServerPassword) - - return client.xenapi[ds0.params['xapi_classname']].get_all_records() + def collect_xen(self, config, ds0, client): + return client.xenapi[ds0.params['xenapi_classname']].get_all_records() def onSuccess(self, results, config): # Create of map of ref to datasource. datasources = dict( - (x.params['xapi_ref'], x) for x in config.datasources) + (x.params['xenapi_ref'], x) for x in config.datasources) data = self.new_data() @@ -144,15 +174,15 @@ def onSuccess(self, results, config): if datasources: LOG.debug( - "missing XAPI data for %s:%s %s", + "missing XenAPI data for %s:%s %s", config.id, - config.datasources[0].params['xapi_classname'], + config.datasources[0].params['xenapi_classname'], datasources.keys()) LOG.debug( - 'success for %s XAPI %s', + 'success for %s XenAPI %s', config.id, - config.datasources[0].params['xapi_classname']) + config.datasources[0].params['xenapi_classname']) data['events'].append(get_event(config, 'successful collection', 0)) @@ -163,9 +193,9 @@ def onError(self, error, config): error = error.value LOG.error( - 'error for %s XAPI %s: %s', + 'error for %s XenAPI %s: %s', config.id, - config.datasources[0].params['xapi_classname'], + config.datasources[0].params['xenapi_classname'], error) data = self.new_data() @@ -173,34 +203,110 @@ def onError(self, error, config): return data -def aggregate_values(datapoint, columns): +class XenAPIEventsPlugin(BasePlugin): ''' - Return column values aggregated according to datapoint configuration. + Collect model updates from the XenAPI events API. ''' - aggregate = { - 'AVERAGE': lambda x: sum(x) / len(x), - 'MAX': max, - 'MIN': min, - 'SUM': sum, - } - return aggregate[datapoint.group_aggregation]([ - aggregate[datapoint.time_aggregation](x) for x in columns]) + @inlineCallbacks + def collect_xen(self, config, ds0, client): + if not hasattr(self, 'producer'): + self.producer = DataMapProducer(client) + maps = yield self.producer.getmaps() -class XenServerRRDDataSourcePlugin(PythonDataSourcePlugin): - proxy_attributes = [ - 'xenserver_addresses', - 'zXenServerUsername', - 'zXenServerPassword', - ] + data = self.new_data() + data['maps'].extend(maps) - @classmethod - def config_key(cls, datasource, context): - return ( - context.device().id, - datasource.getCycleTime(context), - ) + returnValue(data) + + def onSuccess(self, data, config): + LOG.debug('success for %s events', config.id) + data['events'].append(get_event(config, 'successful collection', 0)) + return data + + def onError(self, error, config): + if hasattr(error, 'value'): + error = error.value + + LOG.error('error for %s events: %s', config.id, error) + data = self.new_data() + data['events'].append(get_event(config, str(error), 5)) + return data + + +class XenAPIMessagesPlugin(BasePlugin): + ''' + Collect events from the XenAPI messages API. + ''' + + @inlineCallbacks + def collect_xen(self, config, ds0, client): + message_api = client.xenapi.message + + severity_map = { + '1': 5, + '2': 4, + '3': 3, + '4': 0, + '5': 2, + } + + if not hasattr(self, 'last_datetime'): + self.last_datetime = DateTime('0') + messages = yield message_api.get_all_records() + else: + messages = yield message_api.get_since(self.last_datetime) + + data = self.new_data() + + for ref, message in messages.iteritems(): + if message['timestamp'] > self.last_datetime: + self.last_datetime = message['timestamp'] + + summary = message.get('body') or \ + message.get('name') or \ + 'no body or name provided' + + severity = severity_map.get(message.get('priority', '5'), 2) + + timestamp = message['timestamp'].value.split('Z')[0] + rcvtime = time.mktime( + time.strptime(timestamp, '%Y%m%dT%H:%M:%S')) + + data['events'].append({ + 'device': config.id, + 'component': message.get('obj_uuid'), + 'summary': summary, + 'severity': severity, + 'eventKey': message.get('uuid'), + 'eventClassKey': 'XenServerMessage', + 'rcvtime': rcvtime, + 'xenserver_name': message.get('name'), + 'xenserver_cls': message.get('cls'), + }) + + returnValue(data) + + def onSuccess(self, data, config): + LOG.debug('success for %s messages', config.id) + data['events'].append(get_event(config, 'successful collection', 0)) + return data + + def onError(self, error, config): + if hasattr(error, 'value'): + error = error.value + + LOG.error('error for %s messages: %s', config.id, error) + data = self.new_data() + data['events'].append(get_event(config, str(error), 5)) + return data + + +class XenRRDPlugin(BasePlugin): + ''' + Collects XenServer RRD datasources. + ''' @classmethod def params(cls, datasource, context): @@ -210,14 +316,7 @@ def params(cls, datasource, context): return {} @inlineCallbacks - def collect(self, config): - ds0 = config.datasources[0] - - client = get_client( - ds0.xenserver_addresses, - ds0.zXenServerUsername, - ds0.zXenServerPassword) - + def collect_xen(self, config, ds0, client): rrd_tree = collections.defaultdict( lambda: collections.defaultdict( lambda: collections.defaultdict( diff --git a/ZenPacks/zenoss/XenServer/datasources/XenServerRRDDataSource.py b/ZenPacks/zenoss/XenServer/datasources/XenServerRRDDataSource.py index 13f3262..04e5f3b 100644 --- a/ZenPacks/zenoss/XenServer/datasources/XenServerRRDDataSource.py +++ b/ZenPacks/zenoss/XenServer/datasources/XenServerRRDDataSource.py @@ -36,12 +36,12 @@ class XenServerRRDDataSource(PythonDataSource): # RRDDataSource component = '${here/id}' - cycletime = '${here/zXenServerCollectionInterval}' + cycletime = '${here/zXenServerPerfInterval}' eventClass = '/Ignore' severity = 0 # PythonDataSource - plugin_classname = 'ZenPacks.zenoss.XenServer.datasource_plugins.XenServerRRDDataSourcePlugin' + plugin_classname = 'ZenPacks.zenoss.XenServer.datasource_plugins.XenRRDPlugin' def getDescription(self): ''' diff --git a/ZenPacks/zenoss/XenServer/datasources/XenServerXAPIDataSource.py b/ZenPacks/zenoss/XenServer/datasources/XenServerXenAPIDataSource.py similarity index 61% rename from ZenPacks/zenoss/XenServer/datasources/XenServerXAPIDataSource.py rename to ZenPacks/zenoss/XenServer/datasources/XenServerXenAPIDataSource.py index 87f2afb..697c8e1 100644 --- a/ZenPacks/zenoss/XenServer/datasources/XenServerXAPIDataSource.py +++ b/ZenPacks/zenoss/XenServer/datasources/XenServerXenAPIDataSource.py @@ -24,51 +24,51 @@ import PythonDataSource -class XenServerXAPIDataSource(PythonDataSource): +class XenServerXenAPIDataSource(PythonDataSource): ''' - Datasource used to define XAPI requests for monitoring data. + Datasource used to define XenAPI requests for monitoring data. ''' ZENPACKID = 'ZenPacks.zenoss.XenServer' - sourcetypes = ('XenServer XAPI',) + sourcetypes = ('XenServer XenAPI',) sourcetype = sourcetypes[0] # RRDDataSource component = '${here/id}' - cycletime = '${here/zXenServerCollectionInterval}' + cycletime = '${here/zXenServerPerfInterval}' eventClass = '/Ignore' severity = 0 # PythonDataSource - plugin_classname = 'ZenPacks.zenoss.XenServer.datasource_plugins.XenServerXAPIDataSourcePlugin' + plugin_classname = 'ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIPlugin' - # XenServerXAPIDataSource - xapi_classname = '' - xapi_ref = '${here/xapi_ref}' + # XenServerXenAPIDataSource + xenapi_classname = '' + xenapi_ref = '${here/xenapi_ref}' _properties = PythonDataSource._properties + ( - {'id': 'xapi_classname', 'type': 'string'}, - {'id': 'xapi_ref', 'type': 'string'}, + {'id': 'xenapi_classname', 'type': 'string'}, + {'id': 'xenapi_ref', 'type': 'string'}, ) def getDescription(self): ''' Return short string that represents this datasource. ''' - return self.xapi_classname + return self.xenapi_classname def manage_addRRDDataPoint(self, id, REQUEST=None): ''' Add datapoint to this datasource. - Overridden to create XenServerXAPIDataPoint datapoints. + Overridden to create XenServerXenAPIDataPoint datapoints. ''' if not id: return self.callZenScreen(REQUEST) id = self.prepId(id) - self.datapoints._setObject(id, XenServerXAPIDataPoint(id)) + self.datapoints._setObject(id, XenServerXenAPIDataPoint(id)) datapoint = self.datapoints._getOb(id) if REQUEST: if datapoint: @@ -79,39 +79,39 @@ def manage_addRRDDataPoint(self, id, REQUEST=None): return datapoint -class IXenServerXAPIDataSourceInfo(IRRDDataSourceInfo): +class IXenServerXenAPIDataSourceInfo(IRRDDataSourceInfo): ''' - API Info interface for XenServerXAPIDataSource. + API Info interface for XenServerXenAPIDataSource. ''' - xapi_classname = schema.TextLine( + xenapi_classname = schema.TextLine( group=_t(u'XenServer'), - title=_t(u'XAPI Class Name')) + title=_t(u'XenAPI Class Name')) - xapi_ref = schema.TextLine( + xenapi_ref = schema.TextLine( group=_t(u'XenServer'), - title=_t(u'XAPI Reference')) + title=_t(u'XenAPI Reference')) -class XenServerXAPIDataSourceInfo(RRDDataSourceInfo): +class XenServerXenAPIDataSourceInfo(RRDDataSourceInfo): ''' - API Info adapter factory for XenServerXAPIDataSource. + API Info adapter factory for XenServerXenAPIDataSource. ''' - implements(IXenServerXAPIDataSourceInfo) - adapts(XenServerXAPIDataSource) + implements(IXenServerXenAPIDataSourceInfo) + adapts(XenServerXenAPIDataSource) testable = False - xapi_classname = ProxyProperty('xapi_classname') - xapi_ref = ProxyProperty('xapi_ref') + xenapi_classname = ProxyProperty('xenapi_classname') + xenapi_ref = ProxyProperty('xenapi_ref') -class XenServerXAPIDataPoint(RRDDataPoint): +class XenServerXenAPIDataPoint(RRDDataPoint): ''' Datapoint used to define values to capture from XenAPI responses. - This datapoint will only ever be used by XenServerXAPIDataSource. + This datapoint will only ever be used by XenServerXenAPIDataSource. ''' path = None @@ -123,22 +123,22 @@ class XenServerXAPIDataPoint(RRDDataPoint): ) -class IXenServerXAPIDataPointInfo(IDataPointInfo): +class IXenServerXenAPIDataPointInfo(IDataPointInfo): ''' - API Info interface for XenServerXAPIDataPoint. + API Info interface for XenServerXenAPIDataPoint. ''' path = schema.TextLine(title=_t(u'Path')) rpn = schema.TextLine(title=_t(u'RPN')) -class XenServerXAPIDataPointInfo(DataPointInfo): +class XenServerXenAPIDataPointInfo(DataPointInfo): ''' - API Info adapter factory for XenServerXAPIDataPoint. + API Info adapter factory for XenServerXenAPIDataPoint. ''' - implements(IXenServerXAPIDataPointInfo) - adapts(XenServerXAPIDataPoint) + implements(IXenServerXenAPIDataPointInfo) + adapts(XenServerXenAPIDataPoint) path = ProxyProperty('path') rpn = ProxyProperty('rpn') diff --git a/ZenPacks/zenoss/XenServer/impact.py b/ZenPacks/zenoss/XenServer/impact.py index 1087d4c..8995f8c 100644 --- a/ZenPacks/zenoss/XenServer/impact.py +++ b/ZenPacks/zenoss/XenServer/impact.py @@ -1,79 +1,232 @@ -###################################################################### +############################################################################## # # Copyright (C) Zenoss, Inc. 2013, all rights reserved. # # This content is made available according to terms specified in -# License.zenoss under the directory where your Zenoss product is -# installed. +# License.zenoss under the directory where your Zenoss product is installed. # -###################################################################### +############################################################################## -from zope.component import adapts -from zope.interface import implements -from Products.ZenUtils.guid.interfaces import IGlobalIdentifier -from ZenPacks.zenoss.Impact.impactd import Trigger -from ZenPacks.zenoss.Impact.impactd.relations import ImpactEdge -from ZenPacks.zenoss.Impact.impactd.interfaces import IRelationshipDataProvider -from ZenPacks.zenoss.Impact.impactd.interfaces import INodeTriggers +from ZenPacks.zenoss.XenServer import ZENPACK_NAME +from ZenPacks.zenoss.XenServer.utils import guid +# Lazy imports to make this module not require Impact. +ImpactEdge = None +Trigger = None + +# Constants to avoid typos. AVAILABILITY = 'AVAILABILITY' PERCENT = 'policyPercentageTrigger' THRESHOLD = 'policyThresholdTrigger' -RP = 'ZenPacks.zenoss.XenServer' +DOWN = 'DOWN' +DEGRADED = 'DEGRADED' +ATRISK = 'ATRISK' + -def GUID(obj): - return IGlobalIdentifier(obj).getGUID() +def edge(source, target): + ''' + Create an edge indicating that source impacts target. + source and target are expected to be GUIDs. + ''' + # Lazy import without incurring import overhead. + # http://wiki.python.org/moin/PythonSpeed/PerformanceTips#Import_Statement_Overhead + global ImpactEdge + if not ImpactEdge: + from ZenPacks.zenoss.Impact.impactd.relations import ImpactEdge -def getRedundancyTriggers(guid, format, **kwargs): - """Return a general redundancy set of triggers.""" + return ImpactEdge(source, target, ZENPACK_NAME) - return ( - Trigger(guid, format % 'DOWN', PERCENT, AVAILABILITY, dict( - kwargs, state='DOWN', dependentState='DOWN', threshold='100', - )), - Trigger(guid, format % 'DEGRADED', THRESHOLD, AVAILABILITY, dict( - kwargs, state='DEGRADED', dependentState='DEGRADED', threshold='1', - )), - Trigger(guid, format % 'ATRISK_1', THRESHOLD, AVAILABILITY, dict( - kwargs, state='ATRISK', dependentState='DOWN', threshold='1', - )), - Trigger(guid, format % 'ATRISK_2', THRESHOLD, AVAILABILITY, dict( - kwargs, state='ATRISK', dependentState='ATRISK', threshold='1', - )), - ): +class BaseImpactAdapterFactory(object): + ''' + Abstract base for Impact adapter factories. + ''' -def getPoolTriggers(guid, format, **kwargs): - """Return a general pool set of triggers.""" + def __init__(self, adapted): + self.adapted = adapted - return ( - Trigger(guid, format % 'DOWN', PERCENT, AVAILABILITY, dict( - kwargs, state='DOWN', dependentState='DOWN', threshold='100', - )), - Trigger(guid, format % 'DEGRADED', THRESHOLD, AVAILABILITY, dict( - kwargs, state='DEGRADED', dependentState='DEGRADED', threshold='1', - )), - Trigger(guid, format % 'ATRISK_1', THRESHOLD, AVAILABILITY, dict( - kwargs, state='DEGRADED', dependentState='DOWN', threshold='1', - )), - ) + def guid(self): + if not hasattr(self, '_guid'): + self._guid = guid(self.adapted) + return self._guid -class BaseRelationsProvider(object): - implements(IRelationshipDataProvider) - relationship_provider = RP +class BaseRelationsProvider(BaseImpactAdapterFactory): + ''' + Abstract base for IRelationshipDataProvider adapter factories. + ''' - def __init__(self, adapted): - self._object = adapted + relationship_provider = ZENPACK_NAME + + impact_relationships = None + impacted_by_relationships = None def belongsInImpactGraph(self): return True + def impact(self, relname): + relationship = getattr(self.adapted, relname, None) + if relationship and callable(relationship): + related = relationship() + if not related: + return -class BaseTriggers(object): - implements(INodeTriggers) + try: + for obj in related: + yield edge(self.guid(), guid(obj)) - def __init__(self, adapted): - self._object = adapted + except TypeError: + yield edge(self.guid(), guid(related)) + + def impacted_by(self, relname): + relationship = getattr(self.adapted, relname, None) + if relationship and callable(relationship): + related = relationship() + if not related: + return + + try: + for obj in related: + yield edge(guid(obj), self.guid()) + + except TypeError: + yield edge(guid(related), self.guid()) + + def getEdges(self): + if self.impact_relationships is not None: + for impact_relationship in self.impact_relationships: + for impact in self.impact(impact_relationship): + yield impact + + if self.impacted_by_relationships is not None: + for impacted_by_relationship in self.impacted_by_relationships: + for impacted_by in self.impacted_by(impacted_by_relationship): + yield impacted_by + + +class BaseTriggers(BaseImpactAdapterFactory): + ''' + Abstract base for INodeTriggers adapter factories. + ''' + triggers = [] + + def get_triggers(self): + ''' + Return list of triggers defined by subclass' triggers property. + ''' + # Lazy import without incurring import overhead. + # http://wiki.python.org/moin/PythonSpeed/PerformanceTips#Import_Statement_Overhead + global Trigger + if not Trigger: + from ZenPacks.zenoss.Impact.impactd import Trigger + + for trigger_args in self.triggers: + yield Trigger(self.guid(), *trigger_args) + + +### XenServer Impact Providers ############################################### + +class EndpointRelationsProvider(BaseRelationsProvider): + impact_relationships = ('hosts',) + + +class HostRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ( + 'endpoint', + 'pbds', + 'pifs', + 'suspend_image_sr', + 'crash_dump_sr', + 'local_cache_sr', + 'server_device', + ) + + impact_relationships = ('pool', 'vms', 'cloudstack_host') + + +class NetworkRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('pifs',) + impact_relationships = ('vifs',) + + +class PBDRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('server_disk',) + impact_relationships = ('sr', 'host') + + +class PIFRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('server_interface',) + impact_relationships = ('network', 'host') + + +class PoolRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ( + 'hosts', + 'default_sr', + 'suspend_image_sr', + 'crash_dump_sr', + ) + + impact_relationships = ('vms',) + + +class SRRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('pbds',) + impact_relationships = ( + 'vdis', + 'suspend_image_for_hosts', + 'crash_dump_for_hosts', + 'local_cache_for_hosts', + 'default_for_pools', + 'suspend_image_for_pools', + 'crash_dump_for_pools', + ) + + +class VBDRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('vdi',) + impact_relationships = ('vms', 'guest_disk') + + +class VDIRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('sr',) + impact_relationships = ('vbds',) + + +class VIFRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('network',) + impact_relationships = ('vm', 'guest_interface') + + +class VMRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('host', 'pool', 'vbds', 'vifs') + impact_relationships = ( + 'vmappliances', + 'guest_device', + 'cloudstack_routervm', + 'cloudstack_systemvm', + 'cloudstack_vm', + ) + + +class VMApplianceRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('vms',) + + +### Platform Impact Providers ################################################ + +class DeviceRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('xenserver_vm',) + impact_relationships = ('xenserver_host',) + + +class HardDiskRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('xenserver_vbd',) + impact_relationships = ('xenserver_pbd',) + + +class IpInterfaceRelationsProvider(BaseRelationsProvider): + impacted_by_relationships = ('xenserver_vif',) + impact_relationships = ('xenserver_pif',) diff --git a/ZenPacks/zenoss/XenServer/impact.zcml b/ZenPacks/zenoss/XenServer/impact.zcml deleted file mode 100644 index b904f69..0000000 --- a/ZenPacks/zenoss/XenServer/impact.zcml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/ZenPacks/zenoss/XenServer/lib/XenAPI.py b/ZenPacks/zenoss/XenServer/lib/XenAPI.py deleted file mode 100644 index c7e5277..0000000 --- a/ZenPacks/zenoss/XenServer/lib/XenAPI.py +++ /dev/null @@ -1,192 +0,0 @@ -#============================================================================ -# This library is free software; you can redistribute it and/or -# modify it under the terms of version 2.1 of the GNU Lesser General Public -# License as published by the Free Software Foundation. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -#============================================================================ -# Copyright (C) 2006 XenSource Inc. -#============================================================================ -# -# Parts of this file are based upon xmlrpclib.py, the XML-RPC client -# interface included in the Python distribution. -# -# Copyright (c) 1999-2002 by Secret Labs AB -# Copyright (c) 1999-2002 by Fredrik Lundh -# -# By obtaining, using, and/or copying this software and/or its -# associated documentation, you agree that you have read, understood, -# and will comply with the following terms and conditions: -# -# Permission to use, copy, modify, and distribute this software and -# its associated documentation for any purpose and without fee is -# hereby granted, provided that the above copyright notice appears in -# all copies, and that both that copyright notice and this permission -# notice appear in supporting documentation, and that the name of -# Secret Labs AB or the author not be used in advertising or publicity -# pertaining to distribution of the software without specific, written -# prior permission. -# -# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD -# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- -# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR -# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY -# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS -# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE -# OF THIS SOFTWARE. -# -------------------------------------------------------------------- - -import gettext -import xmlrpclib - - -translation = gettext.translation('xen-xm', fallback = True) - -class Failure(Exception): - def __init__(self, details): - try: - # If this failure is MESSAGE_PARAMETER_COUNT_MISMATCH, then we - # correct the return values here, to account for the fact that we - # transparently add the session handle as the first argument. - if details[0] == 'MESSAGE_PARAMETER_COUNT_MISMATCH': - details[2] = str(int(details[2]) - 1) - details[3] = str(int(details[3]) - 1) - - self.details = details - except Exception, exn: - self.details = ['INTERNAL_ERROR', 'Client-side: ' + str(exn)] - - def __str__(self): - try: - return translation.ugettext(self.details[0]) % self._details_map() - except TypeError, exn: - return "Message database broken: %s.\nXen-API failure: %s" % \ - (exn, str(self.details)) - except Exception, exn: - import sys - print >>sys.stderr, exn - return "Xen-API failure: %s" % str(self.details) - - def _details_map(self): - return dict([(str(i), self.details[i]) - for i in range(len(self.details))]) - - -_RECONNECT_AND_RETRY = (lambda _ : ()) - - -class Session(xmlrpclib.ServerProxy): - """A server proxy and session manager for communicating with Xend using - the Xen-API. - - Example: - - session = Session('http://localhost:9363/') - session.login_with_password('me', 'mypassword') - session.xenapi.VM.start(vm_uuid) - session.xenapi.session.logout() - - For now, this class also supports the legacy XML-RPC API, using - session.xend.domain('Domain-0') and similar. This support will disappear - once there is a working Xen-API replacement for every call in the legacy - API. - """ - - def __init__(self, uri, transport=None, encoding=None, verbose=0, - allow_none=1): - xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding, - verbose, allow_none) - self._session = None - self.last_login_method = None - self.last_login_params = None - - - def xenapi_request(self, methodname, params): - if methodname.startswith('login'): - self._login(methodname, params) - return None - else: - retry_count = 0 - while retry_count < 3: - full_params = (self._session,) + params - result = _parse_result(getattr(self, methodname)(*full_params)) - if result == _RECONNECT_AND_RETRY: - retry_count += 1 - if self.last_login_method: - self._login(self.last_login_method, - self.last_login_params) - else: - raise xmlrpclib.Fault(401, 'You must log in') - else: - return result - raise xmlrpclib.Fault( - 500, 'Tried 3 times to get a valid session, but failed') - - - def _login(self, method, params): - result = _parse_result(getattr(self, 'session.%s' % method)(*params)) - if result == _RECONNECT_AND_RETRY: - raise xmlrpclib.Fault( - 500, 'Received SESSION_INVALID when logging in') - self._session = result - self.last_login_method = method - self.last_login_params = params - - - def __getattr__(self, name): - if name == 'xenapi': - return _Dispatcher(self.xenapi_request, None) - elif name.startswith('login'): - return lambda *params: self._login(name, params) - else: - return xmlrpclib.ServerProxy.__getattr__(self, name) - - -def _parse_result(result): - if type(result) != dict or 'Status' not in result: - raise xmlrpclib.Fault(500, 'Missing Status in response from server' + result) - if result['Status'] == 'Success': - if 'Value' in result: - return result['Value'] - else: - raise xmlrpclib.Fault(500, - 'Missing Value in response from server') - else: - if 'ErrorDescription' in result: - if result['ErrorDescription'][0] == 'SESSION_INVALID': - return _RECONNECT_AND_RETRY - else: - raise Failure(result['ErrorDescription']) - else: - raise xmlrpclib.Fault( - 500, 'Missing ErrorDescription in response from server') - - -# Based upon _Method from xmlrpclib. -class _Dispatcher: - def __init__(self, send, name): - self.__send = send - self.__name = name - - def __repr__(self): - if self.__name: - return '' % self.__name - else: - return '' - - def __getattr__(self, name): - if self.__name is None: - return _Dispatcher(self.__send, name) - else: - return _Dispatcher(self.__send, "%s.%s" % (self.__name, name)) - - def __call__(self, *args): - return self.__send(self.__name, args) diff --git a/ZenPacks/zenoss/XenServer/lib/txxenapi.py b/ZenPacks/zenoss/XenServer/lib/txxenapi.py index 34cf734..da5156b 100644 --- a/ZenPacks/zenoss/XenServer/lib/txxenapi.py +++ b/ZenPacks/zenoss/XenServer/lib/txxenapi.py @@ -8,9 +8,9 @@ ############################################################################## ''' -Twisted library for working with Xen XAPI. +Twisted library for working with the XenServer XenAPI. - http://xenproject.org/developers/teams/xapi.html + http://docs.vmd.citrix.com/XenServer/6.2.0/1.0/en_gb/api/ ''' import collections @@ -28,7 +28,7 @@ LOG = logging.getLogger('txxenapi') -XAPI_CLASSNAMES = [ +XENAPI_CLASSNAMES = [ 'host', 'host_cpu', 'host_metrics', @@ -52,7 +52,7 @@ class Client(object): ''' - XenAPI (XAPI) client. + XenServer XenAPI client. ''' _addresses = None @@ -259,18 +259,18 @@ def sleep(seconds): import sys @inlineCallbacks - def main(address, username, password, xapi_classnames): + def main(address, username, password, xenapi_classnames): client = Client([address], username, password) - for xapi_classname in xapi_classnames: + for xenapi_classname in xenapi_classnames: try: - r = yield getattr(client.xenapi, xapi_classname).get_all_records() + r = yield getattr(client.xenapi, xenapi_classname).get_all_records() except Exception, ex: print >> sys.stderr, ex continue - if len(xapi_classnames) > 1: - filename = '{0}.py'.format(xapi_classname) + if len(xenapi_classnames) > 1: + filename = '{0}.py'.format(xenapi_classname) print "Writing {0}.".format(filename) with open(filename, 'w') as f: pprint.pprint(r, f) @@ -288,11 +288,11 @@ def main(address, username, password, xapi_classnames): sys.exit(1) - xapi_classnames = sys.argv[4:] + xenapi_classnames = sys.argv[4:] - if 'all' in xapi_classnames: - main(sys.argv[1], sys.argv[2], sys.argv[3], XAPI_CLASSNAMES) + if 'all' in xenapi_classnames: + main(sys.argv[1], sys.argv[2], sys.argv[3], XENAPI_CLASSNAMES) else: - main(sys.argv[1], sys.argv[2], sys.argv[3], xapi_classnames) + main(sys.argv[1], sys.argv[2], sys.argv[3], xenapi_classnames) reactor.run() diff --git a/ZenPacks/zenoss/XenServer/modeler/incremental.py b/ZenPacks/zenoss/XenServer/modeler/incremental.py new file mode 100644 index 0000000..eb5a7c0 --- /dev/null +++ b/ZenPacks/zenoss/XenServer/modeler/incremental.py @@ -0,0 +1,391 @@ +############################################################################## +# +# Copyright (C) Zenoss, Inc. 2013, all rights reserved. +# +# This content is made available according to terms specified in +# License.zenoss under the directory where your Zenoss product is installed. +# +############################################################################## + +import logging +LOG = logging.getLogger('zen.XenServer') + +from collections import OrderedDict + +from twisted.internet.defer import inlineCallbacks, returnValue + +from Products.DataCollector.plugins.DataMaps import MultiArgs, ObjectMap, RelationshipMap + +from ZenPacks.zenoss.XenServer import MODULE_NAME +from ZenPacks.zenoss.XenServer.utils import id_from_ref +from ZenPacks.zenoss.XenServer.SR import SR +from ZenPacks.zenoss.XenServer.VDI import VDI +from ZenPacks.zenoss.XenServer.Host import Host +from ZenPacks.zenoss.XenServer.HostCPU import HostCPU +from ZenPacks.zenoss.XenServer.PBD import PBD +from ZenPacks.zenoss.XenServer.PIF import PIF +from ZenPacks.zenoss.XenServer.Network import Network +from ZenPacks.zenoss.XenServer.VM import VM +from ZenPacks.zenoss.XenServer.VBD import VBD +from ZenPacks.zenoss.XenServer.VIF import VIF +from ZenPacks.zenoss.XenServer.VMAppliance import VMAppliance +from ZenPacks.zenoss.XenServer.Pool import Pool + + +__all__ = ['DataMapProducer'] + + +XENAPI_CLASSMAP = { + 'sr': SR, + 'vdi': VDI, + 'host_metrics': Host, + 'host': Host, + 'host_cpu': HostCPU, + 'pbd': PBD, + 'pif_metrics': PIF, + 'pif': PIF, + 'network': Network, + 'vm_metrics': VM, + 'vm': VM, + 'vbd': VBD, + 'vif': VIF, + 'vm_appliance': VMAppliance, + 'pool': Pool, + } + + +def merge_objectmaps(objectmaps): + ''' + Merge attributes from ObjectMaps with identical ids. + ''' + merged = OrderedDict() + for id_, objectmap in ((x.id, x) for x in objectmaps): + if id_ in merged: + objectmap.updateFromDict(merged[id_]) + + merged[id_] = objectmap + + return merged.values() + + +class DataMapProducer(object): + def __init__(self, client): + self.client = client + self.last_token = '' + self.backrefs = {} + self.parentrefs = {} + + @inlineCallbacks + def getmaps(self, timeout=0.0): + ''' + Return a datamaps iterable. + + The first time this method is called the returned iterable will + contain datamaps sufficent to fully model the endpoint. + Subsequent calls will return datamaps sufficient to update the + previous model to the current model. + ''' + records = yield self.client.xenapi.event['from']( + XENAPI_CLASSMAP.keys(), self.last_token, float(timeout)) + + if not records: + returnValue([]) + + # We only care about the most recent event for each ref. + events = OrderedDict() + for event in records['events']: + LOG.debug( + "event operation '%s' processing: %s", + event['operation'], event) + + events[event['ref']] = event + + if self.last_token: + maps = self.incremental_datamaps(events.values()) + else: + maps = self.full_datamaps(events.values()) + + # Store last token for next time. + self.last_token = records['token'] + + returnValue(maps) + + def get_objectmap(self, event): + ''' + Return the appropriate ObjectMap for event. + ''' + model_class = XENAPI_CLASSMAP[event['class']] + + # Building the ObjectMap differs for _metrics classes + # because they unfortunately don't contain their own back- + # reference to the object they provide metrics for. + if event['class'].endswith('_metrics'): + if 'snapshot' not in event: + # No sense processing a metric with no snapshot. + return + + ref = self.backrefs.get(event['ref']) + if not ref: + # The container for this object doesn't exist. + return + + model_method = model_class.objectmap_metrics + else: + ref = event['ref'] + model_method = model_class.objectmap + + # Nested components need some help in knowing their parent. + if 'snapshot' not in event: + parent = self.parentrefs.get(ref) + if parent: + event['snapshot'] = {'parent': parent} + + data = model_method(ref, event.get('snapshot')) + if data: + return ObjectMap(data=data, modname=model_class.__module__) + + def full_datamaps(self, events): + ''' + Return a list of datamaps representing a full model. + ''' + sr_oms = [] + vdi_oms = {} + host_oms = [] + host_cpu_oms = {} + pbd_oms = {} + pif_oms = {} + network_oms = [] + vm_oms = [] + vbd_oms = {} + vif_oms = {} + vm_appliance_oms = [] + pool_oms = [] + + # Used to prevent yielding multiple endpoint.os ObjectMaps. + os_om_flag = False + + for event in events: + if event['operation'] == 'del': + # There should be no delete events in a full model. + continue + + om = self.get_objectmap(event) + if not om: + continue + + if event['class'] == 'sr': + sr_oms.append(om) + + # Initialize contained objmap lists. + vdi_oms.setdefault(event['ref'], []) + + elif event['class'] == 'vdi': + sr_ref = event['snapshot']['SR'] + vdi_oms[sr_ref].append(om) + + # Need to track this to handle subcomponent deletion. + self.parentrefs[event['ref']] = sr_ref + + elif event['class'] == 'host': + host_oms.append(om) + + # Initialize contained objmap lists. + host_cpu_oms.setdefault(event['ref'], []) + pbd_oms.setdefault(event['ref'], []) + pif_oms.setdefault(event['ref'], []) + + # Save metric -> host mapping for host_metrics events. + host_metrics_ref = event['snapshot']['metrics'] + self.backrefs[host_metrics_ref] = event['ref'] + + # Build the endpoint's os ObjectMap from the first host + # data we see. + if not os_om_flag: + os_om_flag = True + yield self.objectmap_endpoint_os(event['snapshot']) + + elif event['class'] == 'host_metrics': + host_oms.append(om) + + elif event['class'] == 'host_cpu': + host_ref = event['snapshot']['host'] + host_cpu_oms[host_ref].append(om) + + # Need to track this to handle subcomponent deletion. + self.parentrefs[event['ref']] = host_ref + + elif event['class'] == 'pbd': + host_ref = event['snapshot']['host'] + pbd_oms[host_ref].append(om) + + # Need to track this to handle subcomponent deletion. + self.parentrefs[event['ref']] = host_ref + + elif event['class'] == 'pif': + host_ref = event['snapshot']['host'] + pif_oms[host_ref].append(om) + + # Save metric -> pif mapping for pif_metrics events. + pif_metrics_ref = event['snapshot']['metrics'] + self.backrefs[pif_metrics_ref] = ( + event['ref'], event['snapshot']['host']) + + # Need to track this to handle subcomponent deletion. + self.parentrefs[event['ref']] = host_ref + + elif event['class'] == 'pif_metrics': + host_ref = self.backrefs[event['ref']][1] + pif_oms[host_ref].append(om) + + elif event['class'] == 'network': + network_oms.append(om) + + elif event['class'] == 'vm': + vm_oms.append(om) + + # Initialize contained objmap lists. + vbd_oms.setdefault(event['ref'], []) + vif_oms.setdefault(event['ref'], []) + + # Save metric -> vm mapping for vm_metrics events. + vm_metrics_ref = event['snapshot']['metrics'] + self.backrefs[vm_metrics_ref] = event['ref'] + + elif event['class'] == 'vm_metrics': + vm_oms.append(om) + + elif event['class'] == 'vbd': + vm_ref = event['snapshot']['VM'] + vbd_oms[vm_ref].append(om) + + # Need to track this to handle subcomponent deletion. + self.parentrefs[event['ref']] = vm_ref + + elif event['class'] == 'vif': + vm_ref = event['snapshot']['VM'] + vif_oms[vm_ref].append(om) + + # Need to track this to handle subcomponent deletion. + self.parentrefs[event['ref']] = vm_ref + + elif event['class'] == 'vm_appliance': + vm_appliance_oms.append(om) + + elif event['class'] == 'pool': + pool_oms.append(om) + + yield RelationshipMap( + relname='srs', + modname=MODULE_NAME['SR'], + objmaps=sr_oms) + + for parent_ref, objmaps in vdi_oms.iteritems(): + yield RelationshipMap( + compname='srs/{}'.format(id_from_ref(parent_ref)), + relname='vdis', + modname=MODULE_NAME['VDI'], + objmaps=objmaps) + + yield RelationshipMap( + relname='hosts', + modname=MODULE_NAME['Host'], + objmaps=merge_objectmaps(host_oms)) + + for parent_ref, objmaps in host_cpu_oms.iteritems(): + yield RelationshipMap( + compname='hosts/{}'.format(id_from_ref(parent_ref)), + relname='hostcpus', + modname=MODULE_NAME['HostCPU'], + objmaps=objmaps) + + for parent_ref, objmaps in pbd_oms.iteritems(): + yield RelationshipMap( + compname='hosts/{}'.format(id_from_ref(parent_ref)), + relname='pbds', + modname=MODULE_NAME['PBD'], + objmaps=objmaps) + + for parent_ref, objmaps in pif_oms.iteritems(): + yield RelationshipMap( + compname='hosts/{}'.format(id_from_ref(parent_ref)), + relname='pifs', + modname=MODULE_NAME['PIF'], + objmaps=merge_objectmaps(objmaps)) + + yield RelationshipMap( + relname='networks', + modname=MODULE_NAME['Network'], + objmaps=network_oms) + + yield RelationshipMap( + relname='vms', + modname=MODULE_NAME['VM'], + objmaps=merge_objectmaps(vm_oms)) + + for parent_ref, objmaps in vbd_oms.iteritems(): + yield RelationshipMap( + compname='vms/{}'.format(id_from_ref(parent_ref)), + relname='vbds', + modname=MODULE_NAME['VBD'], + objmaps=objmaps) + + for parent_ref, objmaps in vif_oms.iteritems(): + yield RelationshipMap( + compname='vms/{}'.format(id_from_ref(parent_ref)), + relname='vifs', + modname=MODULE_NAME['VIF'], + objmaps=objmaps) + + yield RelationshipMap( + relname='vmappliances', + modname=MODULE_NAME['VMAppliance'], + objmaps=vm_appliance_oms) + + yield RelationshipMap( + relname='pools', + modname=MODULE_NAME['Pool'], + objmaps=pool_oms) + + def incremental_datamaps(self, events): + ''' + Generate datamaps representing incremental model updates. + ''' + for event in events: + om = self.get_objectmap(event) + if not om: + continue + + if event['operation'] == 'del': + om.remove = True + yield om + continue + + # Incremental parent ref tracking. + parent_key = { + 'vdi': 'SR', + 'host_cpu': 'host', + 'pbd': 'host', + 'pif': 'host', + 'vbd': 'VM', + 'vif': 'VM', + }.get(event['class']) + + if parent_key: + self.parentrefs[event['ref']] = event['snapshot'][parent_key] + + yield om + + def objectmap_endpoint_os(self, host_properties): + ''' + Return an ObjectMap for endpoint.os given XAPI host properties. + ''' + manufacturer = host_properties.get('API_version_vendor') + version_major = host_properties.get('API_version_major') + version_minor = host_properties.get('API_version_minor') + + if manufacturer and version_major and version_minor: + model = 'XenAPI %s.%s' % (version_major, version_minor) + + return ObjectMap(data={ + 'setProductKey': MultiArgs(model, manufacturer), + }, compname='os') diff --git a/ZenPacks/zenoss/XenServer/modeler/plugins/zenoss/XenServer.py b/ZenPacks/zenoss/XenServer/modeler/plugins/zenoss/XenServer.py index dfc08e9..0efcd40 100644 --- a/ZenPacks/zenoss/XenServer/modeler/plugins/zenoss/XenServer.py +++ b/ZenPacks/zenoss/XenServer/modeler/plugins/zenoss/XenServer.py @@ -8,21 +8,18 @@ ############################################################################## ''' -Model XenServer pools using XenAPI (a.k.a. XAPI). +Model XenServer pools, hosts, PBDs, PIFs, VMs, VBDs, VIFs, +VMAppliances, SRs, VDIs and networks using the XenServer XenAPI. ''' import logging LOG = logging.getLogger('zen.XenServer') -import collections - -from twisted.internet.defer import DeferredList, inlineCallbacks, returnValue +from twisted.internet.defer import inlineCallbacks, returnValue from Products.DataCollector.plugins.CollectorPlugin import PythonPlugin -from Products.DataCollector.plugins.DataMaps import MultiArgs, ObjectMap, RelationshipMap -from Products.ZenUtils.Utils import prepId -from ZenPacks.zenoss.XenServer import MODULE_NAME +from ZenPacks.zenoss.XenServer.modeler.incremental import DataMapProducer from ZenPacks.zenoss.XenServer.utils import add_local_lib_path # Allows txxenapi to be imported. @@ -31,134 +28,7 @@ import txxenapi -XAPI_CLASSES = [ - 'SR', - 'VDI', - 'host_metrics', - 'host', - 'host_cpu', - 'PBD', - 'PIF_metrics', - 'PIF', - 'network', - 'VM_metrics', - 'VM', - 'VBD', - 'VIF', - 'VM_appliance', - 'pool', - ] - - -def id_from_ref(ref): - ''' - Return a component id given a XenAPI OpaqueRef. - ''' - if not ref or ref == 'OpaqueRef:NULL': - return None - - return prepId(ref.split(':', 1)[1]) - - -def ids_from_refs(refs): - ''' - Return list of component ids given a list of XenAPI OpaqueRefs. - - Null references won't be included in the returned list. So it's - possible that the returned list will be shorter than the passed - list. - ''' - ids = [] - - for ref in refs: - id_ = id_from_ref(ref) - if id_: - ids.append(id_) - - return ids - - -def int_or_none(value): - ''' - Return value converted to int or None if conversion fails. - ''' - try: - return int(value) - except (TypeError, ValueError): - return None - - -def float_or_none(value): - ''' - Return value converted to float or None if conversion fails. - ''' - try: - return float(value) - except (TypeError, ValueError): - return None - - -def to_boolean(value, true_value='true'): - ''' - Return value converted to boolean. - ''' - if value == true_value: - return True - else: - return False - - -class ModelerPluginCacheMixin(object): - ''' - Mix-in class to allow modeler plugin instances to safely share an - instance level class per modeling run. - - This is used for when methods need to share data. It is required - because this modeler plugin instance is persistent across modeling - cycles and can be used to model multiple devices. - ''' - - def cache_prepare(self, device): - ''' - Prepare class cache for modeling device. - ''' - self.device = device - if not hasattr(self, '_cache'): - self._cache = {} - - self._cache[self.device.id] = collections.defaultdict(dict) - - def cache_set(self, namespace, key, value): - ''' - Set the cache value for key in namespace. Return value. - ''' - if not self.device or not self._cache: - raise Exception('cache not initialized') - - self._cache[self.device.id][namespace][key] = value - - return value - - def cache_get(self, namespace, key, default=None): - ''' - Get the cached value for key in namespace. - ''' - if not self.device or not self._cache: - raise Exception('cache not initialized') - - return self._cache[self.device.id][namespace].get(key, default) - - def cache_clear(self): - ''' - Clear class cache for current device. - ''' - if self.device and self._cache and self.device.id in self._cache: - del(self._cache[self.device.id]) - - self.device = None - - -class XenServer(PythonPlugin, ModelerPluginCacheMixin): +class XenServer(PythonPlugin): deviceProperties = PythonPlugin.deviceProperties + ( 'xenserver_addresses', 'zXenServerUsername', @@ -189,11 +59,10 @@ def collect(self, device, unused): device.zXenServerUsername, device.zXenServerPassword) - # Simultaneously call client.xenapi.xxx.get_all_records() for - # each XAPI class. + producer = DataMapProducer(client) + try: - results = yield DeferredList([ - client.xenapi[x].get_all_records() for x in XAPI_CLASSES]) + results = yield producer.getmaps() except Exception, ex: LOG.error( "%s %s XenAPI error: %s", @@ -209,609 +78,8 @@ def collect(self, device, unused): returnValue(results) def process(self, device, results, unused): - ''' - Process results of collect method. - - results is a list of two element tuples where the first element - is a boolean indicating whether the call was successful or not, - and the second element is a dictionary keyed by object OpaqueRef - with a dictionary of properties as the value. - - [ - (success, {'OpaqueRef:xxx-xxx': {...}}), - (success, {'OpaqueRef:xxx-xxx': {...}}), - ] - - The list indexes map to the the indexes of XAPI_CLASSES. - ''' if results is None: return None - # Check to see if all requests failed. - if set((x[0], x[1] is not None) for x in results) == [(False, False)]: - LOG.error("No XenServer API response from %s", device.id) - return None - LOG.info("Processing data for device %s", device.id) - - self.cache_prepare(device) - - maps = [] - - # Call self.xxx_relmaps(self, respective_results) for each XAPI - # class. - for i, xapi_class in enumerate(XAPI_CLASSES): - if not results[i][0] or results[i][1] is None: - LOG.error("No %s response from %s", xapi_class, device.id) - continue - - maps.extend( - getattr(self, '%s_relmaps' % xapi_class.lower())( - results[i][1])) - - # Pick up any other maps that can be built with data cached - # during the main process loop above. This allows for modeling - # object not represented 1-for-1 by XAPI_CLASSES. - maps.extend(self.other_maps()) - - self.cache_clear() - - return maps - - def host_metrics_relmaps(self, results): - ''' - Cache host_metrics data to later be used in host_relmaps. - ''' - for ref, properties in results.items(): - self.cache_set('host_metrics', ref, properties) - - # This method needs to be a generator of nothing. - if False: - yield - - def host_relmaps(self, results): - ''' - Yield a single hosts RelationshipMap. - ''' - objmaps = [] - - for ref, properties in results.items(): - title = properties.get('name_label') or properties.get('hostname') - - cpu_info = properties.get('cpu_info', {}) - - cpu_speed = float_or_none(cpu_info.get('speed')) - if cpu_speed: - cpu_speed = cpu_speed * 1048576 # Convert from MHz to Hz. - - metrics = self.cache_get( - 'host_metrics', properties.get('metrics'), {}) - - objmaps.append({ - 'id': id_from_ref(ref), - 'title': title, - 'xapi_ref': ref, - 'xapi_uuid': properties.get('uuid'), - 'xapi_metrics_ref': properties.get('metrics'), - 'api_version_major': properties.get('API_version_major'), - 'api_version_minor': properties.get('API_version_minor'), - 'api_version_vendor': properties.get('API_version_vendor'), - 'address': properties.get('address'), - 'allowed_operations': properties.get('allowed_operations'), - 'capabilities': properties.get('capabilities'), - 'cpu_count': int_or_none(cpu_info.get('cpu_count')), - 'cpu_speed': cpu_speed, - 'edition': properties.get('edition'), - 'enabled': properties.get('enabled'), - 'hostname': properties.get('hostname'), - 'name_description': properties.get('name_description'), - 'name_label': properties.get('name_label'), - 'sched_policy': properties.get('sched_policy'), - 'memory_total': int_or_none(metrics.get('memory_total')), - 'setVMs': ids_from_refs(properties.get('resident_VMs', [])), - 'setSuspendImageSR': id_from_ref(properties.get('suspend_image_sr')), - 'setCrashDumpSR': id_from_ref(properties.get('crash_dump_sr')), - 'setLocalCacheSR': id_from_ref(properties.get('local_cache_sr')), - }) - - # To be used as a default for containing pool with no name. - self.cache_set('host_titles', ref, title) - - # Cache API version information for use in other_maps. - try: - properties = results.itervalues().next() - except StopIteration: - # Who cares if there aren't any hosts? - pass - else: - manufacturer = properties.get('API_version_vendor') - version_major = properties.get('API_version_major') - version_minor = properties.get('API_version_minor') - - if manufacturer and version_major and version_minor: - model = 'XenAPI %s.%s' % (version_major, version_minor) - - self.cache_set('api', 'product', { - 'manufacturer': manufacturer, - 'model': model, - }) - - yield RelationshipMap( - relname='hosts', - modname=MODULE_NAME['Host'], - objmaps=objmaps) - - def host_cpu_relmaps(self, results): - ''' - Yield a hostcpus RelationshipMap for each host. - ''' - objmaps = collections.defaultdict(list) - - for ref, properties in results.items(): - title = properties.get('number') or properties['uuid'] - - cpu_speed = int_or_none(properties.get('speed')) - if cpu_speed: - cpu_speed = cpu_speed * 1048576 # Convert from MHz to Hz. - - objmaps[properties['host']].append({ - 'id': id_from_ref(ref), - 'title': title, - 'xapi_ref': ref, - 'xapi_uuid': properties.get('uuid'), - 'family': int_or_none(properties.get('family')), - 'features': properties.get('features'), - 'flags': properties.get('flags'), - 'model': int_or_none(properties.get('model')), - 'modelname': properties.get('modelname'), - 'number': int_or_none(properties.get('number')), - 'speed': cpu_speed, - 'stepping': int_or_none(properties.get('stepping')), - 'vendor': properties.get('vendor'), - }) - - for parent_ref, grouped_objmaps in objmaps.items(): - yield RelationshipMap( - compname='hosts/%s' % id_from_ref(parent_ref), - relname='hostcpus', - modname=MODULE_NAME['HostCPU'], - objmaps=grouped_objmaps) - - def network_relmaps(self, results): - ''' - Yield a single networks RelationshipMap. - ''' - objmaps = [] - - for ref, properties in results.items(): - title = properties.get('name_label') or properties['uuid'] - - other_config = properties.get('other_config', {}) - - objmaps.append({ - 'id': id_from_ref(ref), - 'title': title, - 'xapi_ref': ref, - 'xapi_uuid': properties.get('uuid'), - 'mtu': properties.get('MTU'), - 'allowed_operations': properties.get('allowed_operations'), - 'bridge': properties.get('bridge'), - 'default_locking_mode': properties.get('default_locking_mode'), - 'name_description': properties.get('name_description'), - 'name_label': properties.get('name_label'), - 'ipv4_begin': other_config.get('ip_begin'), - 'ipv4_end': other_config.get('ip_end'), - 'is_guest_installer_network': to_boolean(other_config.get('is_guest_installer_network')), - 'is_host_internal_management_network': to_boolean(other_config.get('is_host_internal_management_network')), - 'ipv4_netmask': other_config.get('ipv4_netmask'), - 'setPIFs': ids_from_refs(properties.get('PIFs', [])), - 'setVIFs': ids_from_refs(properties.get('VIFs', [])), - }) - - yield RelationshipMap( - relname='networks', - modname=MODULE_NAME['Network'], - objmaps=objmaps) - - def pbd_relmaps(self, results): - ''' - Yield a pbds RelationshipMap for each host. - ''' - objmaps = collections.defaultdict(list) - - for ref, properties in results.items(): - device_config = properties.get('device_config', {}) - - title = device_config.get('location') or \ - device_config.get('device') or \ - properties['uuid'] - - objmaps[properties['host']].append({ - 'id': id_from_ref(ref), - 'title': title, - 'xapi_ref': ref, - 'xapi_uuid': properties.get('uuid'), - 'currently_attached': properties.get('currently_attached'), - 'dc_device': device_config.get('device'), - 'dc_legacy_mode': to_boolean(device_config.get('legacy_mode')), - 'dc_location': device_config.get('location'), - 'setSR': id_from_ref(properties.get('SR')), - }) - - for parent_ref, grouped_objmaps in objmaps.items(): - yield RelationshipMap( - compname='hosts/%s' % id_from_ref(parent_ref), - relname='pbds', - modname=MODULE_NAME['PBD'], - objmaps=grouped_objmaps) - - def pif_metrics_relmaps(self, results): - ''' - Cache PIF_metrics data to later be used in pif_relmaps. - ''' - for ref, properties in results.items(): - self.cache_set('pif_metrics', ref, properties) - - # This method needs to be a generator of nothing. - if False: - yield - - def pif_relmaps(self, results): - ''' - Yield a pifs RelationshipMap for each host. - ''' - objmaps = collections.defaultdict(list) - - for ref, properties in results.items(): - title = properties.get('device') or properties['uuid'] - - # IP is a single string whereas IPv6 is a list. - ipv4_addresses = [x for x in [properties.get('IP')] if x] - ipv6_addresses = [x for x in properties.get('IPv6', []) if x] - - vlan = properties.get('VLAN') - if vlan == '-1': - vlan = None - - metrics = self.cache_get( - 'pif_metrics', properties.get('metrics'), {}) - - speed = int_or_none(metrics.get('speed')) - if speed: - speed = speed * 1e6 # Convert from Mbps to bps. - - objmaps[properties['host']].append({ - 'id': id_from_ref(ref), - 'title': title, - 'xapi_ref': ref, - 'xapi_metrics_ref': properties.get('metrics'), - 'xapi_uuid': properties.get('uuid'), - 'dns': properties.get('dns'), - 'ipv4_addresses': ipv4_addresses, - 'ipv6_addresses': ipv6_addresses, - 'macaddress': properties.get('MAC'), - 'mtu': properties.get('MTU'), - 'vlan': vlan, - 'carrier': metrics.get('carrier'), - 'currently_attached': properties.get('currently_attached'), - 'pif_device': properties.get('device'), - 'pif_device_id': metrics.get('device_id'), - 'pif_device_name': metrics.get('device_name'), - 'disallow_unplug': properties.get('disallow_unplug'), - 'ipv4_gateway': properties.get('gateway'), - 'ipv4_configuration_mode': properties.get('ip_configuration_mode'), - 'ipv6_configuration_mode': properties.get('ipv6_configuration_mode'), - 'ipv6_gateway': properties.get('ipv6_gateway'), - 'management': properties.get('management'), - 'ipv4_netmask': properties.get('netmask'), - 'physical': properties.get('physical'), - 'primary_address_type': properties.get('primary_address_type'), - 'speed': speed, - 'vendor_name': metrics.get('vendor_name'), - 'setNetwork': id_from_ref(properties.get('network')), - }) - - for parent_ref, grouped_objmaps in objmaps.items(): - yield RelationshipMap( - compname='hosts/%s' % id_from_ref(parent_ref), - relname='pifs', - modname=MODULE_NAME['PIF'], - objmaps=grouped_objmaps) - - def pool_relmaps(self, results): - ''' - Yield a single pools RelationshipMap. - ''' - objmaps = [] - - for ref, properties in results.items(): - pool_title = properties.get('name_label') - - # By default pools will not have a name_label. XenCenter - # shows the master host's name_label in this case. We should - # do the same. - if not pool_title: - master_ref = properties.get('master') - if master_ref: - pool_title = self.cache_get('host_titles', master_ref) - - other_config = properties.get('other_config', {}) - - objmaps.append({ - 'id': id_from_ref(ref), - 'title': pool_title, - 'xapi_ref': ref, - 'xapi_uuid': properties.get('uuid'), - 'ha_allow_overcommit': properties.get('ha_allow_overcommit'), - 'ha_enabled': properties.get('ha_enabled'), - 'ha_host_failures_to_tolerate': int_or_none(properties.get('ha_host_failures_to_tolerate')), - 'name_description': properties.get('name_description'), - 'name_label': properties.get('name_label'), - 'oc_cpuid_feature_mask': other_config.get('cpuid_feature_mask'), - 'oc_memory_ratio_hvm': other_config.get('memory-ratio-hvm'), - 'oc_memory_ratio_pv': other_config.get('memory-ratio-pv'), - 'vswitch_controller': properties.get('vswitch_controller'), - 'setMaster': id_from_ref(properties.get('master')), - 'setDefaultSR': id_from_ref(properties.get('default_SR')), - 'setSuspendImageSR': id_from_ref(properties.get('suspend_image_SR')), - 'setCrashDumpSR': id_from_ref(properties.get('crash_dump_SR')), - }) - - yield RelationshipMap( - relname='pools', - modname=MODULE_NAME['Pool'], - objmaps=objmaps) - - def sr_relmaps(self, results): - ''' - Yield a single srs RelationshipMap. - ''' - objmaps = [] - - for ref, properties in results.items(): - title = properties.get('name_label') or properties['uuid'] - - sm_config = properties.get('sm_config', {}) - - objmaps.append({ - 'id': id_from_ref(ref), - 'title': title, - 'xapi_ref': ref, - 'xapi_uuid': properties.get('uuid'), - 'allowed_operations': properties.get('allowed_operations'), - 'content_type': properties.get('content_type'), - 'local_cache_enabled': properties.get('local_cache_enabled'), - 'name_description': properties.get('name_description'), - 'name_label': properties.get('name_label'), - 'physical_size': properties.get('physical_size'), - 'shared': properties.get('shared'), - 'sm_type': sm_config.get('type'), - 'sr_type': properties.get('type'), - 'setPBDs': ids_from_refs(properties.get('PBDs', [])) - }) - - yield RelationshipMap( - relname='srs', - modname=MODULE_NAME['SR'], - objmaps=objmaps) - - def vbd_relmaps(self, results): - ''' - Yield a vbds RelationshipMap for each VM. - ''' - objmaps = collections.defaultdict(list) - - for ref, properties in results.items(): - title = properties.get('device') or \ - properties.get('userdevice') or \ - properties['uuid'] - - objmaps[properties['VM']].append({ - 'id': id_from_ref(ref), - 'title': title, - 'xapi_ref': ref, - 'xapi_metrics_ref': properties.get('metrics'), - 'xapi_uuid': properties.get('uuid'), - 'allowed_operations': properties.get('allowed_operations'), - 'bootable': properties.get('bootable'), - 'currently_attached': properties.get('currently_attached'), - 'vbd_device': properties.get('device'), - 'empty': properties.get('empty'), - 'mode': properties.get('mode'), - 'storage_lock': properties.get('storage_lock'), - 'vbd_type': properties.get('type'), - 'unpluggable': properties.get('unpluggable'), - 'userdevice': properties.get('userdevice'), - 'setVDI': id_from_ref(properties.get('VDI')), - }) - - for parent_ref, grouped_objmaps in objmaps.items(): - yield RelationshipMap( - compname='vms/%s' % id_from_ref(parent_ref), - relname='vbds', - modname=MODULE_NAME['VBD'], - objmaps=grouped_objmaps) - - def vdi_relmaps(self, results): - ''' - Yield a vdis RelationshipMap for each storage repository. - ''' - objmaps = collections.defaultdict(list) - - for ref, properties in results.items(): - title = properties.get('name_label') or \ - properties.get('location') or \ - properties['uuid'] - - objmaps[properties['SR']].append({ - 'id': id_from_ref(ref), - 'title': title, - 'xapi_ref': ref, - 'xapi_uuid': properties.get('uuid'), - 'allow_caching': properties.get('allow_caching'), - 'allowed_operations': properties.get('allowed_operations'), - 'is_a_snapshot': properties.get('is_a_snapshot'), - 'location': properties.get('location'), - 'managed': properties.get('managed'), - 'missing': properties.get('missing'), - 'name_description': properties.get('name_description'), - 'name_label': properties.get('name_label'), - 'on_boot': properties.get('on_boot'), - 'read_only': properties.get('read_only'), - 'sharable': properties.get('sharable'), - 'storage_lock': properties.get('storage_lock'), - 'vdi_type': properties.get('type'), - 'virtual_size': properties.get('virtual_size'), - 'setVBDs': ids_from_refs(properties.get('VBDs', [])), - }) - - for ref, ref_objmaps in objmaps.items(): - yield RelationshipMap( - compname='srs/%s' % id_from_ref(ref), - relname='vdis', - modname=MODULE_NAME['VDI'], - objmaps=ref_objmaps) - - def vif_relmaps(self, results): - ''' - Yield a vifs RelationshipMap for each VM. - ''' - objmaps = collections.defaultdict(list) - - for ref, properties in results.items(): - title = properties.get('device') or properties['uuid'] - - objmaps[properties['VM']].append({ - 'id': id_from_ref(ref), - 'title': title, - 'xapi_ref': ref, - 'xapi_metrics_ref': properties.get('metrics'), - 'xapi_uuid': properties.get('uuid'), - 'macaddress': properties.get('MAC'), - 'mac_autogenerated': properties.get('MAC_autogenerated'), - 'mtu': properties.get('MTU'), - 'allowed_operations': properties.get('allowed_operations'), - 'currently_attached': properties.get('currently_attached'), - 'vif_device': properties.get('device'), - 'ipv4_allowed': properties.get('ipv4_allowed'), - 'ipv6_allowed': properties.get('ipv6_allowed'), - 'locking_mode': properties.get('locking_mode'), - 'setNetwork': id_from_ref(properties.get('network')), - }) - - for parent_ref, grouped_objmaps in objmaps.items(): - yield RelationshipMap( - compname='vms/%s' % id_from_ref(parent_ref), - relname='vifs', - modname=MODULE_NAME['VIF'], - objmaps=grouped_objmaps) - - def vm_metrics_relmaps(self, results): - ''' - Cache VM_metrics data to later be used in vm_relmaps. - ''' - for ref, properties in results.items(): - self.cache_set('vm_metrics', ref, properties) - - # This method needs to be a generator of nothing. - if False: - yield - - def vm_relmaps(self, results): - ''' - Yield a single vms RelationshipMap. - ''' - objmaps = [] - - for ref, properties in results.items(): - if properties.get('is_a_snapshot') or \ - properties.get('is_snapshot_from_vmpp') or \ - properties.get('is_a_template'): - - continue - - title = properties.get('name_label') or properties['uuid'] - - guest_metrics_ref = properties.get('guest_metrics') - if guest_metrics_ref == 'OpaqueRef:NULL': - guest_metrics_ref = None - - metrics = self.cache_get( - 'vm_metrics', properties.get('metrics'), {}) - - objmaps.append({ - 'id': id_from_ref(ref), - 'title': title, - 'xapi_ref': ref, - 'xapi_metrics_ref': properties.get('metrics'), - 'xapi_guest_metrics_ref': guest_metrics_ref, - 'xapi_uuid': properties.get('uuid'), - 'hvm_shadow_multiplier': properties.get('HVM_shadow_multiplier'), - 'vcpus_at_startup': int_or_none(properties.get('VCPUs_at_startup')), - 'vcpus_max': int_or_none(properties.get('VCPUs_max')), - 'actions_after_crash': properties.get('actions_after_crash'), - 'actions_after_reboot': properties.get('actions_after_reboot'), - 'actions_after_shutdown': properties.get('actions_after_shutdown'), - 'allowed_operations': properties.get('allowed_operations'), - 'domarch': properties.get('domarch'), - 'domid': int_or_none(properties.get('domid')), - 'ha_always_run': properties.get('ha_always_run'), - 'ha_restart_priority': properties.get('ha_restart_priority'), - 'is_a_snapshot': properties.get('is_a_snapshot'), - 'is_a_template': properties.get('is_a_template'), - 'is_control_domain': properties.get('is_control_domain'), - 'is_snapshot_from_vmpp': properties.get('is_snapshot_from_vmpp'), - 'memory_actual': int_or_none(metrics.get('memory_actual')), - 'name_description': properties.get('name_description'), - 'name_label': properties.get('name_label'), - 'power_state': properties.get('power_state'), - 'shutdown_delay': int_or_none(properties.get('shutdown_delay')), - 'start_delay': int_or_none(properties.get('start_delay')), - 'user_version': int_or_none(properties.get('user_version')), - 'version': int_or_none(properties.get('version')), - 'setHost': id_from_ref(properties.get('resident_on')), - 'setVMAppliance': id_from_ref(properties.get('appliance')), - }) - - yield RelationshipMap( - relname='vms', - modname=MODULE_NAME['VM'], - objmaps=objmaps) - - def vm_appliance_relmaps(self, results): - ''' - Yield a single vmappliances RelationshipMap. - ''' - objmaps = [] - - for ref, properties in results.items(): - title = properties.get('name_label') or properties['uuid'] - - objmaps.append({ - 'id': id_from_ref(ref), - 'title': title, - 'xapi_ref': ref, - 'xapi_uuid': properties.get('uuid'), - 'allowed_operations': properties.get('allowed_operations'), - 'name_description': properties.get('name_description'), - 'name_label': properties.get('name_label'), - 'setVMs': ids_from_refs(properties.get('VMs', [])), - }) - - yield RelationshipMap( - relname='vmappliances', - modname=MODULE_NAME['VMAppliance'], - objmaps=objmaps) - - def other_maps(self): - ''' - Yield DataMaps that don't map directly to one of the - XAPI_CLASSES through their respective *_relmaps methods. - ''' - # This information is gathered from the host call. So it should - # be cached in the host_relmaps method. - api = self.cache_get('api', 'product') - if api: - yield ObjectMap(compname='os', data={ - 'setProductKey': MultiArgs(api['model'], api['manufacturer']), - }) + return tuple(results) diff --git a/ZenPacks/zenoss/XenServer/modeler/plugins/zenoss/http/__init__.py b/ZenPacks/zenoss/XenServer/modeler/plugins/zenoss/http/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ZenPacks/zenoss/XenServer/objects/objects.xml b/ZenPacks/zenoss/XenServer/objects/objects.xml index fc83f43..098a60f 100644 --- a/ZenPacks/zenoss/XenServer/objects/objects.xml +++ b/ZenPacks/zenoss/XenServer/objects/objects.xml @@ -3,10 +3,10 @@ -XenAPI (XAPI) Endpoints +XenServer Pools and Hosts (XenAPI Endpoints) -XenAPI (XAPI) Endpoints +XenServer Pools and Hosts (XenAPI Endpoints) ['zenoss.XenServer'] @@ -15,7 +15,7 @@ XenAPI (XAPI) Endpoints True -[] +['Endpoint'] ZenPacks.zenoss.XenServer.Endpoint @@ -24,14 +24,61 @@ ZenPacks.zenoss.XenServer.Endpoint True + + +ZenPacks.zenoss.XenServer.Endpoint + + + + +Python + + +True + + +/Ignore + + +0 + + +${here/zXenServerModelInterval} + + +ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIEventsPlugin + + + + +Python + + +True + + +/Ignore + + +0 + + +${here/zXenServerEventsInterval} + + +ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIMessagesPlugin + + + + ZenPacks.zenoss.XenServer.Host - + -XenServer XAPI +XenServer XenAPI True @@ -46,19 +93,19 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerXAPIDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIPlugin - + host - -${here/xapi_ref} + +${here/xenapi_ref} - + GAUGE @@ -88,10 +135,10 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerRRDDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenRRDPlugin @@ -1017,10 +1064,10 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerRRDDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenRRDPlugin @@ -1121,9 +1168,9 @@ ZenPacks.zenoss.XenServer.PBD ZenPacks.zenoss.XenServer.PIF - + -XenServer XAPI +XenServer XenAPI True @@ -1138,19 +1185,19 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerXAPIDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIPlugin - + PIF_metrics - -${here/xapi_metrics_ref} + +${here/xenapi_metrics_ref} - + GAUGE @@ -1183,10 +1230,10 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerRRDDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenRRDPlugin @@ -1294,7 +1341,7 @@ metric_speed AVERAGE - + 1 @@ -1320,45 +1367,13 @@ ${graphPoint/id} -1 -metric_ioRead - - -AVERAGE - - - - -3 - - -00cc00 - - -LINE - - -1 - - -False - - -%7.2lf%s - - -${graphPoint/id} - - --1 - - rrd_rx AVERAGE - + 2 @@ -1384,38 +1399,6 @@ ${graphPoint/id} -1 -metric_ioWrite - - -AVERAGE - - - - -4 - - -0000ff - - -LINE - - -1 - - -False - - -%7.2lf%s - - -${graphPoint/id} - - --1 - - rrd_tx @@ -1431,9 +1414,9 @@ AVERAGE ZenPacks.zenoss.XenServer.Pool - + -XenServer XAPI +XenServer XenAPI True @@ -1448,19 +1431,19 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerXAPIDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIPlugin - + pool - -${here/xapi_ref} + +${here/xenapi_ref} - + GAUGE @@ -1471,7 +1454,7 @@ True ha_host_failures_to_tolerate - + GAUGE @@ -1588,9 +1571,9 @@ AVERAGE ZenPacks.zenoss.XenServer.SR - + -XenServer XAPI +XenServer XenAPI True @@ -1605,19 +1588,19 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerXAPIDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIPlugin - + SR - -${here/xapi_ref} + +${here/xenapi_ref} - + GAUGE @@ -1628,7 +1611,7 @@ True physical_size - + GAUGE @@ -1639,7 +1622,7 @@ True physical_utilisation - + GAUGE @@ -1805,10 +1788,10 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerRRDDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenRRDPlugin @@ -1945,9 +1928,9 @@ AVERAGE ZenPacks.zenoss.XenServer.VDI - + -XenServer XAPI +XenServer XenAPI True @@ -1962,19 +1945,19 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerXAPIDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIPlugin - + VDI - -${here/xapi_ref} + +${here/xenapi_ref} - + GAUGE @@ -1985,7 +1968,7 @@ True physical_utilisation - + GAUGE @@ -2037,7 +2020,7 @@ True 00cc00 -LINE +AREA 1 @@ -2119,10 +2102,10 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerRRDDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenRRDPlugin @@ -2265,9 +2248,9 @@ AVERAGE ZenPacks.zenoss.XenServer.VM - + -XenServer XAPI +XenServer XenAPI True @@ -2282,19 +2265,19 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerXAPIDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIPlugin - + VM_metrics - -${here/xapi_metrics_ref} + +${here/xenapi_metrics_ref} - + GAUGE @@ -2305,7 +2288,7 @@ True memory_actual - + GAUGE @@ -2318,9 +2301,9 @@ VCPUs_number - + -XenServer XAPI +XenServer XenAPI True @@ -2335,19 +2318,19 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerXAPIDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIPlugin - + VM - -${here/xapi_ref} + +${here/xenapi_ref} - + GAUGE @@ -2377,10 +2360,10 @@ ${here/id} 0 -${here/zXenServerCollectionInterval} +${here/zXenServerPerfInterval} -ZenPacks.zenoss.XenServer.datasource_plugins.XenServerRRDDataSourcePlugin +ZenPacks.zenoss.XenServer.datasource_plugins.XenRRDPlugin @@ -2997,6 +2980,11 @@ AVERAGE + + +ZenPacks.zenoss.XenServer.VMAppliance + + ZenPacks.zenoss.XenServer.VM @@ -3025,7 +3013,27 @@ XenServerCollectionSuccess 10 -XAPI: successful collection +XenAPI: successful collection + + + + + + + +from ZenPacks.zenoss.XenServer import transforms +evt.component = transforms.get_component_id(dmd, evt.component) + + +XenServerMessage + + +10 + + +VM 'ubuntu-test-1' started on host: xenserver1 (uuid: 10fda11b-67e5-439b-80d3-54d63fa5d8dd) + + diff --git a/ZenPacks/zenoss/XenServer/patches/__init__.py b/ZenPacks/zenoss/XenServer/patches/__init__.py new file mode 100644 index 0000000..051a341 --- /dev/null +++ b/ZenPacks/zenoss/XenServer/patches/__init__.py @@ -0,0 +1,27 @@ +############################################################################## +# +# Copyright (C) Zenoss, Inc. 2013, all rights reserved. +# +# This content is made available according to terms specified in +# License.zenoss under the directory where your Zenoss product is installed. +# +############################################################################## + +from importlib import import_module + + +def optional_import(module_name, patch_module_name): + ''' + Import patch_module_name only if module_name is importable. + ''' + try: + import_module(module_name) + except ImportError: + pass + else: + import_module( + '.{0}'.format(patch_module_name), + 'ZenPacks.zenoss.XenServer.patches') + + +optional_import('Products.ZenModel', 'platform') diff --git a/ZenPacks/zenoss/XenServer/patches/platform.py b/ZenPacks/zenoss/XenServer/patches/platform.py new file mode 100644 index 0000000..97c53c4 --- /dev/null +++ b/ZenPacks/zenoss/XenServer/patches/platform.py @@ -0,0 +1,205 @@ +############################################################################## +# +# Copyright (C) Zenoss, Inc. 2013, all rights reserved. +# +# This content is made available according to terms specified in +# License.zenoss under the directory where your Zenoss product is installed. +# +############################################################################## + +import logging +ADM_LOG = logging.getLogger("zen.ApplyDataMap") + +from Products.DataCollector.plugins.DataMaps import ObjectMap +from Products.ZenEvents import Event +from Products.ZenEvents.ZenEventClasses import Change_Remove, Change_Remove_Blocked +from Products.ZenModel.Lockable import Lockable +from Products.ZenUtils.Utils import monkeypatch + +from ZenPacks.zenoss.XenServer.PIF import PIF +from ZenPacks.zenoss.XenServer.VIF import VIF + + +def device_macaddresses(device): + ''' + Return all MAC addresses associated with device. + ''' + macaddresses = [] + cat = device.dmd.ZenLinkManager._getCatalog(layer=2) + if cat is not None: + brains = cat(deviceId=device.getPrimaryId()) + macaddresses.extend(b.macaddress for b in brains if b.macaddress) + + return macaddresses + + +@monkeypatch('Products.ZenModel.Device.Device') +def xenserver_host(self): + ''' + Return the XenServer Host running on this device. + ''' + pif = PIF.findByMAC(self.dmd, device_macaddresses(self)) + if pif: + return pif.host() + + +@monkeypatch('Products.ZenModel.Device.Device') +def xenserver_vm(self): + ''' + Return the XenServer VM on which this device is a guest. + ''' + vif = VIF.findByMAC(self.dmd, device_macaddresses(self)) + if vif: + return vif.vm() + + +@monkeypatch('Products.ZenModel.HardDisk.HardDisk') +def xenserver_pbd(self): + ''' + Return the XenServer PBD using this disk. + ''' + xenserver_host = self.device().xenserver_host() + if xenserver_host: + pbd_dc_device = '/dev/{}'.format(self.id) + for pbd in xenserver_host.pbds(): + if pbd.dc_device and pbd.dc_device == pbd_dc_device: + return pbd + + +@monkeypatch('Products.ZenModel.HardDisk.HardDisk') +def xenserver_vbd(self): + ''' + Return the XenServer VBD underlying this disk. + ''' + xenserver_vm = self.device().xenserver_vm() + if xenserver_vm: + for vbd in xenserver_vm.vbds(): + if vbd.vbd_device and vbd.vbd_device == self.id: + return vbd + + +@monkeypatch('Products.ZenModel.IpInterface.IpInterface') +def xenserver_pif(self): + ''' + Return the XenServer PIF using this interface. + ''' + return PIF.findByMAC(self.dmd, self.macaddress) + + +@monkeypatch('Products.ZenModel.IpInterface.IpInterface') +def xenserver_vif(self): + ''' + Return the XenServer VIF underlying this interface. + ''' + return VIF.findByMAC(self.dmd, self.macaddress) + + +@monkeypatch('Products.Zuul.routers.device.DeviceRouter') +def getComponentTree(self, uid=None, id=None, **kwargs): + ''' + Retrieves all of the components set up to be used in a + tree. + + Overridden to sort XenServer component types in a reasonable way. + + @type uid: string + @param uid: Unique identifier of the root of the tree to retrieve + @type id: string + @param id: not used + @rtype: [dictionary] + @return: Component properties in tree form + ''' + # original is injected by monkeypatch decorator. + result = original(self, uid=uid, id=id, **kwargs) + + if self._getFacade().getInfo(uid=uid).meta_type != 'XenServerEndpoint': + return result + + order = [ + 'XenServerPool', + 'XenServerHost', + 'XenServerHostCPU', + 'XenServerPBD', + 'XenServerPIF', + 'XenServerSR', + 'XenServerVDI', + 'XenServerNetwork', + 'XenServerVMAppliance', + 'XenServerVM', + 'XenServerVBD', + 'XenServerVIF', + ] + + return sorted(result, key=lambda x: order.index(x['id'])) + + +@monkeypatch('Products.DataCollector.ApplyDataMap.ApplyDataMap') +def _updateRelationship(self, device, relmap): + ''' + Add/Update/Remove objects to the target relationship. + + Overridden to catch naked ObjectMaps with relname set. This + indicates an incremental model that should be specially handled. + + Return True if a change was made or false if no change was made. + ''' + if not isinstance(relmap, ObjectMap): + # original is injected by monkeypatch decorator. + return original(self, device, relmap) + + remove = getattr(relmap, 'remove', False) is True + if hasattr(relmap, 'remove'): + del(relmap.remove) + + relname = getattr(relmap, 'relname', None) + if hasattr(relmap, 'relname'): + del(relmap.relname) + + rel = getattr(device, relname, None) + if not rel: + return False + + if remove: + return self._removeRelObject(device, relmap, relname) + + obj = rel._getOb(relmap.id, None) + if obj: + return self._updateObject(obj, relmap) + + return self._createRelObject(device, relmap, relname)[0] + + +@monkeypatch('Products.DataCollector.ApplyDataMap.ApplyDataMap') +def _removeRelObject(self, device, objmap, relname): + ''' + Remove an object in a relationship using its ObjectMap. + + Return True if a change was made or False if no change was made. + ''' + rel = getattr(device, relname, None) + if not rel: + return False + + obj = rel._getOb(objmap.id, None) + if not obj: + return False + + if isinstance(obj, Lockable) and obj.isLockedFromDeletion(): + msg = "Deletion Blocked: {} '{}' on {}".format( + obj.meta_type, obj.id, obj.device().id) + + ADM_LOG.warn(msg) + if obj.sendEventWhenBlocked(): + self.logEvent( + device, obj, Change_Remove_Blocked, msg, Event.Warning) + + return False + + self.logChange( + device, obj, Change_Remove, + "removing object {} from rel {} on {}".format( + obj.id, relname, device.id)) + + rel._delObject(obj.id) + + return True diff --git a/ZenPacks/zenoss/XenServer/resources/img/cluster.png b/ZenPacks/zenoss/XenServer/resources/img/cluster.png new file mode 100644 index 0000000..18f34b5 Binary files /dev/null and b/ZenPacks/zenoss/XenServer/resources/img/cluster.png differ diff --git a/ZenPacks/zenoss/XenServer/resources/img/host.png b/ZenPacks/zenoss/XenServer/resources/img/host.png new file mode 100644 index 0000000..59b4965 Binary files /dev/null and b/ZenPacks/zenoss/XenServer/resources/img/host.png differ diff --git a/ZenPacks/zenoss/XenServer/resources/img/storage-domain.png b/ZenPacks/zenoss/XenServer/resources/img/storage-domain.png new file mode 100644 index 0000000..6f199c2 Binary files /dev/null and b/ZenPacks/zenoss/XenServer/resources/img/storage-domain.png differ diff --git a/ZenPacks/zenoss/XenServer/resources/img/virtual-disk.png b/ZenPacks/zenoss/XenServer/resources/img/virtual-disk.png new file mode 100644 index 0000000..c9f906e Binary files /dev/null and b/ZenPacks/zenoss/XenServer/resources/img/virtual-disk.png differ diff --git a/ZenPacks/zenoss/XenServer/resources/img/virtual-network-interface.png b/ZenPacks/zenoss/XenServer/resources/img/virtual-network-interface.png new file mode 100644 index 0000000..b0e194e Binary files /dev/null and b/ZenPacks/zenoss/XenServer/resources/img/virtual-network-interface.png differ diff --git a/ZenPacks/zenoss/XenServer/resources/img/virtual-server.png b/ZenPacks/zenoss/XenServer/resources/img/virtual-server.png new file mode 100644 index 0000000..c50680c Binary files /dev/null and b/ZenPacks/zenoss/XenServer/resources/img/virtual-server.png differ diff --git a/ZenPacks/zenoss/XenServer/resources/img/xenserver.png b/ZenPacks/zenoss/XenServer/resources/img/xenserver.png new file mode 100644 index 0000000..1c175da Binary files /dev/null and b/ZenPacks/zenoss/XenServer/resources/img/xenserver.png differ diff --git a/ZenPacks/zenoss/XenServer/resources/js/endpoint.js b/ZenPacks/zenoss/XenServer/resources/js/endpoint.js index 7f157b8..50e2289 100644 --- a/ZenPacks/zenoss/XenServer/resources/js/endpoint.js +++ b/ZenPacks/zenoss/XenServer/resources/js/endpoint.js @@ -64,11 +64,29 @@ Ext.apply(Zenoss.render, { isLink = true; } + var related_suffix = ''; + if (obj.meta_type == 'XenServerHost' && record.data.server_device) { + related_suffix = ' (' + 'server)'; + } else if (obj.meta_type == 'XenServerPBD' && record.data.server_disk) { + related_suffix = ' (' + 'on server)'; + } else if (obj.meta_type == 'XenServerPIF' && record.data.server_interface) { + related_suffix = ' (' + 'on server)'; + } else if (obj.meta_type == 'XenServerVBD' && record.data.guest_disk) { + related_suffix = ' (' + 'on guest)'; + } else if (obj.meta_type == 'XenServerVIF' && record.data.guest_interface) { + related_suffix = ' (' + 'on guest)'; + } else if (obj.meta_type == 'XenServerVM' && record.data.guest_device) { + related_suffix = ' (' + 'guest)'; + } + + var link = null; if (isLink) { - return ''+obj.title+''; + link = ''+obj.title+''; } else { - return obj.title; + link = obj.title; } + + return link + related_suffix; } }); @@ -164,14 +182,15 @@ ZC.XenServerHostPanel = Ext.extend(ZC.XenServerComponentGridPanel, { {name: 'meta_type'}, {name: 'status'}, {name: 'severity'}, - {name: 'usesMonitorAttribute'}, + {name: 'server_device'}, // for name {name: 'pool'}, {name: 'is_pool_master'}, {name: 'address'}, - {name: 'cpu_count'}, // for cpu_combined - {name: 'cpu_speed'}, // for cpu_combined + {name: 'cpu_count'}, // for cpu_combined + {name: 'cpu_speed'}, // for cpu_combined {name: 'memory_total'}, {name: 'vm_count'}, + {name: 'usesMonitorAttribute'}, {name: 'monitor'}, {name: 'monitored'}, {name: 'locking'} @@ -431,6 +450,7 @@ ZC.XenServerPBDPanel = Ext.extend(ZC.XenServerComponentGridPanel, { {name: 'meta_type'}, {name: 'status'}, {name: 'severity'}, + {name: 'server_disk'}, // for name {name: 'host'}, {name: 'sr'}, {name: 'currently_attached'}, @@ -528,6 +548,7 @@ ZC.XenServerPIFPanel = Ext.extend(ZC.XenServerComponentGridPanel, { {name: 'meta_type'}, {name: 'status'}, {name: 'severity'}, + {name: 'server_interface'}, // for name {name: 'host'}, {name: 'network'}, {name: 'primary_address_type'}, // for address @@ -846,6 +867,7 @@ ZC.XenServerVBDPanel = Ext.extend(ZC.XenServerComponentGridPanel, { {name: 'meta_type'}, {name: 'status'}, {name: 'severity'}, + {name: 'guest_disk'}, // for name {name: 'vm'}, {name: 'vdi'}, {name: 'vbd_type'}, @@ -1078,6 +1100,7 @@ ZC.XenServerVIFPanel = Ext.extend(ZC.XenServerComponentGridPanel, { {name: 'meta_type'}, {name: 'status'}, {name: 'severity'}, + {name: 'guest_interface'}, // for name {name: 'vm'}, {name: 'network'}, {name: 'locking_mode'}, @@ -1175,6 +1198,7 @@ ZC.XenServerVMPanel = Ext.extend(ZC.XenServerComponentGridPanel, { {name: 'meta_type'}, {name: 'status'}, {name: 'severity'}, + {name: 'guest_device'}, // for name {name: 'host'}, {name: 'vmappliance'}, {name: 'vcpus_at_startup'}, // for combined_cpu diff --git a/ZenPacks/zenoss/XenServer/resources/js/global.js b/ZenPacks/zenoss/XenServer/resources/js/global.js index e879930..d584eb3 100644 --- a/ZenPacks/zenoss/XenServer/resources/js/global.js +++ b/ZenPacks/zenoss/XenServer/resources/js/global.js @@ -6,6 +6,7 @@ var ZE = Ext.ns('Zenoss.extensions'); /* Friendly Names for Component Types ***************************************/ +ZC.registerName('XenServerEndpoint', _t('Endpoint'), _t('Endpoints')); ZC.registerName('XenServerHost', _t('Host'), _t('Hosts')); ZC.registerName('XenServerHostCPU', _t('Host CPU'), _t('Host CPUs')); ZC.registerName('XenServerNetwork', _t('Network'), _t('Networks')); @@ -14,22 +15,22 @@ ZC.registerName('XenServerPIF', _t('Physical NIC'), _t('Physical NICs')); ZC.registerName('XenServerPool', _t('Pool'), _t('Pools')); ZC.registerName('XenServerSR', _t('Storage Repository'), _t('Storage Repositories')); ZC.registerName('XenServerVBD', _t('Virtual Block Device'), _t('Virtual Block Devices')); -ZC.registerName('XenServerVDI', _t('Virtual Disk'), _t('Virtual Disks')); +ZC.registerName('XenServerVDI', _t('Virtual Disk Image'), _t('Virtual Disk Images')); ZC.registerName('XenServerVIF', _t('Virtual NIC'), _t('Virtual NICs')); ZC.registerName('XenServerVM', _t('VM'), _t('VMs')); ZC.registerName('XenServerVMAppliance', _t('vApp'), _t('vApps')); -/* "Add XenServer" Dialog ***************************************************/ +/* "Add XenServer Endpoint" Dialog ******************************************/ var add_xenserver = new Zenoss.Action({ - text: _t('Add XenServer') + '...', + text: _t('Add XenServer Endpoint') + '...', id: 'add_xenserver-item', permission: 'Manage DMD', handler: function(btn, e) { var win = new Zenoss.dialog.CloseDialog({ width: 400, - title: _t('Add XenServer'), + title: _t('Add XenServer Endpoint'), items: [{ xtype: 'form', buttonAlign: 'left', diff --git a/ZenPacks/zenoss/XenServer/tests/.gitignore b/ZenPacks/zenoss/XenServer/tests/.gitignore deleted file mode 100644 index 1269488..0000000 --- a/ZenPacks/zenoss/XenServer/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -data diff --git a/ZenPacks/zenoss/XenServer/tests/test_datasource_plugins.py b/ZenPacks/zenoss/XenServer/tests/test_datasource_plugins.py index 731d528..ca1d6a4 100644 --- a/ZenPacks/zenoss/XenServer/tests/test_datasource_plugins.py +++ b/ZenPacks/zenoss/XenServer/tests/test_datasource_plugins.py @@ -57,10 +57,12 @@ def test_aggregate_values(self): for group_f, time_f, v, expected in cases: datapoint = Mock() - datapoint.params = { - 'group_aggregation': group_f, - 'time_aggregation': time_f, - } + datapoint.group_aggregation = group_f + datapoint.time_aggregation = time_f + # datapoint.params = { + # 'group_aggregation': group_f, + # 'time_aggregation': time_f, + # } r = aggregate_values(datapoint, v) diff --git a/ZenPacks/zenoss/XenServer/tests/test_impact.py b/ZenPacks/zenoss/XenServer/tests/test_impact.py new file mode 100644 index 0000000..7b733a9 --- /dev/null +++ b/ZenPacks/zenoss/XenServer/tests/test_impact.py @@ -0,0 +1,678 @@ +############################################################################## +# +# Copyright (C) Zenoss, Inc. 2013, all rights reserved. +# +# This content is made available according to terms specified in +# License.zenoss under the directory where your Zenoss product is installed. +# +############################################################################## + +''' +Unit test for all-things-Impact. +''' + +import transaction + +from zope.component import subscribers + +from Products.Five import zcml + +from Products.ZenTestCase.BaseTestCase import BaseTestCase +from Products.ZenUtils.guid.interfaces import IGUIDManager +from Products.ZenUtils.Utils import monkeypatch + +from ZenPacks.zenoss.XenServer.utils import guid, require_zenpack +from ZenPacks.zenoss.XenServer.tests.utils import ( + add_contained, add_noncontained, + ) + + +@monkeypatch('Products.Zuul') +def get_dmd(): + ''' + Retrieve the DMD object. Handle unit test connection oddities. + + This has to be monkeypatched on Products.Zuul instead of + Products.Zuul.utils because it's already imported into Products.Zuul + by the time this monkeypatch happens. + ''' + try: + # original is injected by the monkeypatch decorator. + return original() + + except AttributeError: + connections = transaction.get()._synchronizers.data.values()[:] + for cxn in connections: + app = cxn.root()['Application'] + if hasattr(app, 'zport'): + return app.zport.dmd + + +def impacts_for(thing): + ''' + Return a two element tuple. + + First element is a list of object ids impacted by thing. Second element is + a list of object ids impacting thing. + ''' + from ZenPacks.zenoss.Impact.impactd.interfaces \ + import IRelationshipDataProvider + + impacted_by = [] + impacting = [] + + guid_manager = IGUIDManager(thing.getDmd()) + for subscriber in subscribers([thing], IRelationshipDataProvider): + for edge in subscriber.getEdges(): + if edge.source == guid(thing): + impacted_by.append(guid_manager.getObject(edge.impacted).id) + elif edge.impacted == guid(thing): + impacting.append(guid_manager.getObject(edge.source).id) + + return (impacted_by, impacting) + + +def triggers_for(thing): + ''' + Return a dictionary of triggers for thing. + + Returned dictionary keys will be triggerId of a Trigger instance and + values will be the corresponding Trigger instance. + ''' + from ZenPacks.zenoss.Impact.impactd.interfaces import INodeTriggers + + triggers = {} + + for sub in subscribers((thing,), INodeTriggers): + for trigger in sub.get_triggers(): + triggers[trigger.triggerId] = trigger + + return triggers + + +def create_endpoint(dmd): + ''' + Return an Endpoint suitable for Impact functional testing. + ''' + # DeviceClass + dc = dmd.Devices.createOrganizer('/XenServer') + dc.setZenProperty('zPythonClass', 'ZenPacks.zenoss.XenServer.Endpoint') + + # Endpoint + endpoint = dc.createInstance('endpoint') + + # Host + from ZenPacks.zenoss.XenServer.Host import Host + host1 = add_contained(endpoint, 'hosts', Host('host1')) + + from ZenPacks.zenoss.XenServer.PBD import PBD + pbd1 = add_contained(host1, 'pbds', PBD('pbd1')) + + from ZenPacks.zenoss.XenServer.PIF import PIF + pif1 = add_contained(host1, 'pifs', PIF('pif1')) + + # Storage + from ZenPacks.zenoss.XenServer.SR import SR + sr1 = add_contained(endpoint, 'srs', SR('sr1')) + add_noncontained(sr1, 'pbds', pbd1) + add_noncontained(sr1, 'suspend_image_for_hosts', host1) + + sr2 = add_contained(endpoint, 'srs', SR('sr2')) + add_noncontained(sr2, 'crash_dump_for_hosts', host1) + + sr3 = add_contained(endpoint, 'srs', SR('sr3')) + add_noncontained(sr3, 'local_cache_for_hosts', host1) + + from ZenPacks.zenoss.XenServer.VDI import VDI + vdi1 = add_contained(sr1, 'vdis', VDI('vdi1')) + + # Network + from ZenPacks.zenoss.XenServer.Network import Network + network1 = add_contained(endpoint, 'networks', Network('network1')) + add_noncontained(network1, 'pifs', pif1) + + # Pool + from ZenPacks.zenoss.XenServer.Pool import Pool + pool1 = add_contained(endpoint, 'pools', Pool('pool1')) + add_noncontained(pool1, 'master', host1) + add_noncontained(pool1, 'default_sr', sr1) + add_noncontained(pool1, 'suspend_image_sr', sr2) + add_noncontained(pool1, 'crash_dump_sr', sr3) + + # VM + from ZenPacks.zenoss.XenServer.VM import VM + vm1 = add_contained(endpoint, 'vms', VM('vm1')) + add_noncontained(vm1, 'host', host1) + + from ZenPacks.zenoss.XenServer.VBD import VBD + vbd1 = add_contained(vm1, 'vbds', VBD('vbd1')) + add_noncontained(vbd1, 'vdi', vdi1) + + from ZenPacks.zenoss.XenServer.VIF import VIF + vif1 = add_contained(vm1, 'vifs', VIF('vif1')) + add_noncontained(vif1, 'network', network1) + + # vApp + from ZenPacks.zenoss.XenServer.VMAppliance import VMAppliance + vapp1 = add_contained(endpoint, 'vmappliances', VMAppliance('vapp1')) + add_noncontained(vapp1, 'vms', vm1) + + return endpoint + + +class TestImpact(BaseTestCase): + def afterSetUp(self): + super(TestImpact, self).afterSetUp() + + import Products.ZenEvents + zcml.load_config('meta.zcml', Products.ZenEvents) + + try: + import ZenPacks.zenoss.DynamicView + zcml.load_config('configure.zcml', ZenPacks.zenoss.DynamicView) + except ImportError: + return + + try: + import ZenPacks.zenoss.Impact + zcml.load_config('meta.zcml', ZenPacks.zenoss.Impact) + zcml.load_config('configure.zcml', ZenPacks.zenoss.Impact) + except ImportError: + return + + import ZenPacks.zenoss.XenServer + zcml.load_config('configure.zcml', ZenPacks.zenoss.XenServer) + + def endpoint(self): + ''' + Return a XenServer endpoint device populated in a suitable way + for Impact testing. + ''' + if not hasattr(self, '_endpoint'): + self._endpoint = create_endpoint(self.dmd) + + return self._endpoint + + def assertTriggersExist(self, triggers, expected_trigger_ids): + ''' + Assert that each expected_trigger_id exists in triggers. + ''' + for trigger_id in expected_trigger_ids: + self.assertTrue( + trigger_id in triggers, 'missing trigger: %s' % trigger_id) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_Endpoint(self): + endpoint = self.endpoint() + + impacts, impacted_by = impacts_for(endpoint) + + # Endpoint -> Host + self.assertTrue( + 'host1' in impacts, + 'missing impact: {} <- {}'.format('host1', endpoint)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_Host(self): + host1 = self.endpoint().getObjByPath('hosts/host1') + + impacts, impacted_by = impacts_for(host1) + + # Endpoint -> Host + self.assertTrue( + 'endpoint' in impacted_by, + 'missing impact: {} -> {}'.format('endpoint', host1)) + + # PBD -> Host + self.assertTrue( + 'pbd1' in impacted_by, + 'missing impact: {} -> {}'.format('pbd1', host1)) + + # SR -> Host + self.assertTrue( + 'sr1' in impacted_by, + 'missing impact: {} -> {}'.format('sr1', host1)) + + self.assertTrue( + 'sr2' in impacted_by, + 'missing impact: {} -> {}'.format('sr2', host1)) + + self.assertTrue( + 'sr3' in impacted_by, + 'missing impact: {} -> {}'.format('sr3', host1)) + + # PIF -> Host + self.assertTrue( + 'pif1' in impacted_by, + 'missing impact: {} -> {}'.format('pif1', host1)) + + # Host -> Pool + self.assertTrue( + 'pool1' in impacts, + 'missing impact: {} <- {}'.format('pool1', host1)) + + # Host -> VM + self.assertTrue( + 'vm1' in impacts, + 'missing impact: {} <- {}'.format('vm1', host1)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_Network(self): + network1 = self.endpoint().getObjByPath('networks/network1') + + impacts, impacted_by = impacts_for(network1) + + # PIF -> Network + self.assertTrue( + 'pif1' in impacted_by, + 'missing impact: {} -> {}'.format('pif1', network1)) + + # Network -> VIF + self.assertTrue( + 'vif1' in impacts, + 'missing impact: {} <- {}'.format('vif1', network1)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_PBD(self): + pbd1 = self.endpoint().getObjByPath('hosts/host1/pbds/pbd1') + + impacts, impacted_by = impacts_for(pbd1) + + # PBD -> SR + self.assertTrue( + 'sr1' in impacts, + 'missing impact: {} <- {}'.format('sr1', pbd1)) + + # PBD -> Host + self.assertTrue( + 'host1' in impacts, + 'missing impact: {} <- {}'.format('host1', pbd1)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_PIF(self): + pif1 = self.endpoint().getObjByPath('hosts/host1/pifs/pif1') + + impacts, impacted_by = impacts_for(pif1) + + # PIF -> Network + self.assertTrue( + 'network1' in impacts, + 'missing impact: {} <- {}'.format('network1', pif1)) + + # PIF -> Host + self.assertTrue( + 'host1' in impacts, + 'missing impact: {} <- {}'.format('host1', pif1)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_Pool(self): + pool1 = self.endpoint().getObjByPath('pools/pool1') + + impacts, impacted_by = impacts_for(pool1) + + # Host -> Pool + self.assertTrue( + 'host1' in impacted_by, + 'missing impact: {} -> {}'.format('host1', pool1)) + + # SR -> Pool + self.assertTrue( + 'sr1' in impacted_by, + 'missing impact: {} -> {}'.format('sr1', pool1)) + + self.assertTrue( + 'sr2' in impacted_by, + 'missing impact: {} -> {}'.format('sr2', pool1)) + + self.assertTrue( + 'sr3' in impacted_by, + 'missing impact: {} -> {}'.format('sr3', pool1)) + + # Pool -> VM + self.assertTrue( + 'vm1' in impacts, + 'missing impact: {} <- {}'.format('vm1', pool1)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_SR(self): + sr1 = self.endpoint().getObjByPath('srs/sr1') + sr2 = self.endpoint().getObjByPath('srs/sr2') + sr3 = self.endpoint().getObjByPath('srs/sr3') + + sr1_impacts, sr1_impacted_by = impacts_for(sr1) + sr2_impacts, sr2_impacted_by = impacts_for(sr2) + sr3_impacts, sr3_impacted_by = impacts_for(sr3) + + # PBD -> SR + self.assertTrue( + 'pbd1' in sr1_impacted_by, + 'missing impact: {} -> {}'.format('pbd1', sr1)) + + # SR -> Host + self.assertTrue( + 'host1' in sr1_impacts, + 'missing impacts: {} <- {}'.format('host1', sr1)) + + self.assertTrue( + 'host1' in sr2_impacts, + 'missing impacts: {} <- {}'.format('host1', sr2)) + + self.assertTrue( + 'host1' in sr3_impacts, + 'missing impacts: {} <- {}'.format('host1', sr3)) + + # SR -> Pool + self.assertTrue( + 'pool1' in sr1_impacts, + 'missing impacts: {} <- {}'.format('pool1', sr1)) + + self.assertTrue( + 'pool1' in sr2_impacts, + 'missing impacts: {} <- {}'.format('pool1', sr2)) + + self.assertTrue( + 'pool1' in sr3_impacts, + 'missing impacts: {} <- {}'.format('pool1', sr3)) + + # SR -> VDI + self.assertTrue( + 'vdi1' in sr1_impacts, + 'missing impacts: {} <- {}'.format('vdi1', sr1)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_VBD(self): + vbd1 = self.endpoint().getObjByPath('vms/vm1/vbds/vbd1') + + impacts, impacted_by = impacts_for(vbd1) + + # VDI -> VBD + self.assertTrue( + 'vdi1' in impacted_by, + 'missing impact: {} -> {}'.format('vdi1', vbd1)) + + # VBD -> VM + self.assertTrue( + 'vm1' in impacts, + 'missing impact: {} <- {}'.format('vm1', vbd1)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_VDI(self): + vdi1 = self.endpoint().getObjByPath('srs/sr1/vdis/vdi1') + + impacts, impacted_by = impacts_for(vdi1) + + # SR -> VDI + self.assertTrue( + 'sr1' in impacted_by, + 'missing impact: {} -> {}'.format('sr1', vdi1)) + + # VDI -> VBD + self.assertTrue( + 'vbd1' in impacts, + 'missing impact: {} <- {}'.format('vbd1', vdi1)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_VIF(self): + vif1 = self.endpoint().getObjByPath('vms/vm1/vifs/vif1') + + impacts, impacted_by = impacts_for(vif1) + + # Network -> VIF + self.assertTrue( + 'network1' in impacted_by, + 'missing impact: {} -> {}'.format('network1', vif1)) + + # VIF -> VM + self.assertTrue( + 'vm1' in impacts, + 'missing impact: {} <- {}'.format('vm1', vif1)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_VM(self): + vm1 = self.endpoint().getObjByPath('vms/vm1') + + impacts, impacted_by = impacts_for(vm1) + + # Host -> VM + self.assertTrue( + 'host1' in impacted_by, + 'missing impact: {} -> {}'.format('host1', vm1)) + + # Pool -> VM + self.assertTrue( + 'pool1' in impacted_by, + 'missing impact: {} -> {}'.format('pool1', vm1)) + + # VBD -> VM + self.assertTrue( + 'vbd1' in impacted_by, + 'missing impact: {} -> {}'.format('vbd1', vm1)) + + # VIF -> VM + self.assertTrue( + 'vif1' in impacted_by, + 'missing impact: {} -> {}'.format('vif1', vm1)) + + # VM -> VMAppliance + self.assertTrue( + 'vapp1' in impacts, + 'missing impact: {} <- {}'.format('vapp1', vm1)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_VMAppliance(self): + vapp1 = self.endpoint().getObjByPath('vmappliances/vapp1') + + impacts, impacted_by = impacts_for(vapp1) + + # VM -> VMAppliance + self.assertTrue( + 'vm1' in impacted_by, + 'missing impact: {} -> {}'.format('vm1', vapp1)) + + ### Platform ############################################################# + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_Platform_Physical(self): + linux_dc = self.dmd.Devices.createOrganizer('/Server/Linux') + linux_server = linux_dc.createInstance('test-linux-host1') + + from Products.ZenModel.IpInterface import IpInterface + linux_iface = add_contained(linux_server.os, 'interfaces', IpInterface('eth0')) + linux_iface.macaddress = '00:0c:29:fe:ab:bc' + linux_iface.index_object() + + from Products.ZenModel.HardDisk import HardDisk + linux_disk = add_contained(linux_server.hw, 'harddisks', HardDisk('sda3')) + + host1 = self.endpoint().getObjByPath('hosts/host1') + host1.address = '192.168.66.71' + + pif1 = self.endpoint().getObjByPath('hosts/host1/pifs/pif1') + pif1.pif_device = linux_iface.id + pif1.macaddress = linux_iface.macaddress + pif1.index_object() + + pbd1 = self.endpoint().getObjByPath('hosts/host1/pbds/pbd1') + pbd1.dc_device = '/dev/{}'.format(linux_disk.id) + + host1_impacts, host1_impacted_by = impacts_for(host1) + pbd1_impacts, pbd1_impacted_by = impacts_for(pbd1) + pif1_impacts, pif1_impacted_by = impacts_for(pif1) + + server_impacts, server_impacted_by = impacts_for(linux_server) + iface_impacts, iface_impacted_by = impacts_for(linux_iface) + disk_impacts, disk_impacted_by = impacts_for(linux_disk) + + # Physical Server -> Host + self.assertTrue( + linux_server.id in host1_impacted_by, + 'missing impact: {} -> {}'.format(linux_server, host1)) + + self.assertTrue( + host1.id in server_impacts, + 'missing impact: {} <- {}'.format(host1, linux_server)) + + # Physical Server IpInterface -> PIF + self.assertTrue( + linux_iface.id in pif1_impacted_by, + 'missing impact: {} -> {}'.format(linux_iface, pif1)) + + self.assertTrue( + pif1.id in iface_impacts, + 'missing impact: {} <- {}'.format(pif1, linux_iface)) + + # Physical Server HardDisk -> PBD + self.assertTrue( + linux_disk.id in pbd1_impacted_by, + 'missing impact: {} -> {}'.format(linux_disk, pbd1)) + + self.assertTrue( + pbd1.id in disk_impacts, + 'missing impact: {} <- {}'.format(pbd1, linux_disk)) + + @require_zenpack('ZenPacks.zenoss.Impact') + def test_Platform_Virtual(self): + linux_dc = self.dmd.Devices.createOrganizer('/Server/Linux') + linux_server = linux_dc.createInstance('test-linux-guest1') + + from Products.ZenModel.IpInterface import IpInterface + linux_iface = add_contained(linux_server.os, 'interfaces', IpInterface('eth0')) + linux_iface.macaddress = '00:0c:29:fe:ab:bc' + linux_iface.index_object() + + from Products.ZenModel.HardDisk import HardDisk + linux_disk = add_contained(linux_server.hw, 'harddisks', HardDisk('xvda')) + + vm1 = self.endpoint().getObjByPath('vms/vm1') + + vif1 = self.endpoint().getObjByPath('vms/vm1/vifs/vif1') + vif1.vif_device = linux_iface.id + vif1.macaddress = linux_iface.macaddress + vif1.index_object() + + vbd1 = self.endpoint().getObjByPath('vms/vm1/vbds/vbd1') + vbd1.vbd_device = linux_disk.id + + vm1_impacts, vm1_impacted_by = impacts_for(vm1) + vbd1_impacts, vbd1_impacted_by = impacts_for(vbd1) + vif1_impacts, vif1_impacted_by = impacts_for(vif1) + + server_impacts, server_impacted_by = impacts_for(linux_server) + iface_impacts, iface_impacted_by = impacts_for(linux_iface) + disk_impacts, disk_impacted_by = impacts_for(linux_disk) + + # VM -> Guest Device + self.assertTrue( + vm1.id in server_impacted_by, + 'missing impact: {} -> {}'.format(vm1, linux_server)) + + self.assertTrue( + linux_server.id in vm1_impacts, + 'missing impact: {} <- {}'.format(linux_server, vm1)) + + # PIF -> Guest IpInterface + self.assertTrue( + vif1.id in iface_impacted_by, + 'missing impact: {} -> {}'.format(vif1, linux_iface)) + + self.assertTrue( + linux_iface.id in vif1_impacts, + 'missing impact: {} <- {}'.format(linux_iface, vif1)) + + # PBD -> Guest HardDisk + self.assertTrue( + vbd1.id in disk_impacted_by, + 'missing impact: {} -> {}'.format(vbd1, linux_disk)) + + self.assertTrue( + linux_disk.id in vbd1_impacts, + 'missing impact: {} <- {}'.format(linux_disk, vbd1)) + + ### CloudStack ########################################################### + + @require_zenpack('ZenPacks.zenoss.Impact') + @require_zenpack('ZenPacks.zenoss.CloudStack') + def test_CloudStack(self): + try: + from ZenPacks.zenoss.CloudStack.tests.test_impact import create_cloud + except ImportError: + # CloudStack earlier than 1.1 which doesn't have hooks for + # XenServer impact. + return + + from ZenPacks.zenoss.XenServer.VM import VM + from ZenPacks.zenoss.XenServer.VIF import VIF + + # Create CloudStack configuration. + cs_cloud = create_cloud(self.dmd) + + cs_host = cs_cloud.getObjByPath('zones/zone1/pods/pod1/clusters/cluster1/hosts/host1') + cs_host.ip_address = '10.11.12.13' + cs_host.index_object() + + cs_routervm = cs_cloud.getObjByPath('zones/zone1/pods/pod1/routervms/routervm1') + cs_routervm.linklocal_macaddress = '00:0c:29:fe:ab:bc' + cs_routervm.index_object() + + cs_systemvm = cs_cloud.getObjByPath('zones/zone1/pods/pod1/systemvms/systemvm1') + cs_systemvm.linklocal_macaddress = '00:0c:29:fe:ab:bd' + cs_systemvm.index_object() + + cs_vm = cs_cloud.getObjByPath('zones/zone1/vms/vm1') + cs_vm.mac_address = '00:0c:29:fe:ab:be' + cs_vm.index_object() + + # Create XenServer configuration. + xen_endpoint = self.endpoint() + + xen_host = xen_endpoint.getObjByPath('hosts/host1') + xen_pif = xen_host.getObjByPath('pifs/pif1') + xen_pif.ipv4_addresses = [cs_host.ip_address] + xen_pif.index_object() + + xen_routervm = add_contained(xen_endpoint, 'vms', VM('xen_routervm1')) + xen_routervm_vif = add_contained(xen_routervm, 'vifs', VIF('xen_routervm1_vif1')) + xen_routervm_vif.macaddress = cs_routervm.linklocal_macaddress + xen_routervm_vif.index_object() + + xen_systemvm = add_contained(xen_endpoint, 'vms', VM('xen_systemvm1')) + xen_systemvm_vif = add_contained(xen_systemvm, 'vifs', VIF('xen_systemvm1_vif1')) + xen_systemvm_vif.macaddress = cs_systemvm.linklocal_macaddress + xen_systemvm_vif.index_object() + + xen_vm = xen_endpoint.getObjByPath('vms/vm1') + xen_vm_vif = xen_vm.getObjByPath('vifs/vif1') + xen_vm_vif.macaddress = cs_vm.mac_address + xen_vm_vif.index_object() + + xen_host_impacts, xen_host_impacted_by = impacts_for(xen_host) + xen_vm_impacts, vm_impacted_by = impacts_for(xen_vm) + xen_routervm_impacts, xen_routervm_impacted_by = impacts_for(xen_routervm) + xen_systemvm_impacts, xen_systemvm_impacted_by = impacts_for(xen_systemvm) + + # Host -> CloudStack Host + self.assertTrue( + cs_host.id in xen_host_impacts, + 'missing impact: {0} <- {1}'.format(cs_host, xen_host)) + + # VM -> CloudStack RouterVM + self.assertTrue( + cs_routervm.id in xen_routervm_impacts, + 'missing impact: {0} <- {1}'.format(cs_routervm, xen_routervm)) + + # VM -> CloudStack SystemVM + self.assertTrue( + cs_systemvm.id in xen_systemvm_impacts, + 'missing impact: {0} <- {1}'.format(cs_systemvm, xen_systemvm)) + + # VM -> CloudStack VirtualMachine + self.assertTrue( + cs_vm.id in xen_vm_impacts, + 'missing impact: {0} <- {1}'.format(cs_vm, xen_vm)) + + +def test_suite(): + from unittest import TestSuite, makeSuite + suite = TestSuite() + suite.addTest(makeSuite(TestImpact)) + return suite diff --git a/ZenPacks/zenoss/XenServer/tests/utils.py b/ZenPacks/zenoss/XenServer/tests/utils.py new file mode 100644 index 0000000..3a30958 --- /dev/null +++ b/ZenPacks/zenoss/XenServer/tests/utils.py @@ -0,0 +1,25 @@ +############################################################################## +# +# Copyright (C) Zenoss, Inc. 2013, all rights reserved. +# +# This content is made available according to terms specified in +# License.zenoss under the directory where your Zenoss product is installed. +# +############################################################################## + + +def add_contained(obj, relname, target): + ''' + Add and return obj to containing relname on target. + ''' + rel = getattr(obj, relname) + rel._setObject(target.id, target) + return rel._getOb(target.id) + + +def add_noncontained(obj, relname, target): + ''' + Add obj to non-containing relname on target. + ''' + rel = getattr(obj, relname) + rel.addRelation(target) diff --git a/ZenPacks/zenoss/XenServer/transforms.py b/ZenPacks/zenoss/XenServer/transforms.py new file mode 100644 index 0000000..1b3967e --- /dev/null +++ b/ZenPacks/zenoss/XenServer/transforms.py @@ -0,0 +1,24 @@ +############################################################################## +# +# Copyright (C) Zenoss, Inc. 2013, all rights reserved. +# +# This content is made available according to terms specified in +# License.zenoss under the directory where your Zenoss product is installed. +# +############################################################################## + +from ZenPacks.zenoss.XenServer.utils import BaseComponent + + +def get_component_id(dmd, xenapi_uuid): + ''' + Return the Zenoss component ID for a XenServer component given its + XenAPI UUID. + + Returns the given xenapi_uuid if no XenServer component can be found. + ''' + component = BaseComponent.findByUUID(dmd, xenapi_uuid) + if component: + return component.id + + return xenapi_uuid diff --git a/ZenPacks/zenoss/XenServer/utils.py b/ZenPacks/zenoss/XenServer/utils.py index b076363..5695dee 100644 --- a/ZenPacks/zenoss/XenServer/utils.py +++ b/ZenPacks/zenoss/XenServer/utils.py @@ -8,6 +8,12 @@ # ###################################################################### +import logging +LOG = logging.getLogger('zen.XenServer') + +import functools +import importlib + from zope.event import notify from Products.AdvancedQuery import Eq, Or @@ -16,6 +22,9 @@ from Products.ZenModel.DeviceComponent import DeviceComponent from Products.ZenModel.ManagedEntity import ManagedEntity from Products.ZenModel.ZenossSecurity import ZEN_CHANGE_DEVICE +from Products.ZenRelations.ToManyContRelationship import ToManyContRelationship +from Products.ZenUtils.guid.interfaces import IGlobalIdentifier +from Products.ZenUtils.Search import makeFieldIndex, makeKeywordIndex from Products.ZenUtils.Utils import prepId from Products import Zuul from Products.Zuul.catalog.events import IndexingEvent @@ -37,6 +46,47 @@ def add_local_lib_path(): site.addsitedir(os.path.join(os.path.dirname(__file__), 'lib')) +def guid(obj): + ''' + Return GUID for obj. + ''' + return IGlobalIdentifier(obj).getGUID() + + +def require_zenpack(zenpack_name, default=None): + ''' + Decorator with mandatory zenpack_name argument. + + If zenpack_name can't be imported, the decorated function or method + will return default. Otherwise it will execute and return as + written. + + Usage looks like the following: + + @require_zenpack('ZenPacks.zenoss.Impact') + @require_zenpack('ZenPacks.zenoss.vCloud') + def dothatthingyoudo(args): + return "OK" + + @require_zenpack('ZenPacks.zenoss.Impact', []) + def returnalistofthings(args): + return [1, 2, 3] + ''' + def wrap(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + try: + importlib.import_module(zenpack_name) + except ImportError: + return + + return f(*args, **kwargs) + + return wrapper + + return wrap + + def updateToMany(relationship, root, type_, ids): ''' Update ToMany relationship given search root, type and ids. @@ -63,9 +113,41 @@ def updateToMany(relationship, root, type_, ids): if id_ in new_ids: relationship.addRelation(obj) + + # Index remote object. It might have a custom path reporter. + notify(IndexingEvent(obj, 'path', False)) else: relationship.removeRelation(obj) + # If the object was not deleted altogether.. + if not isinstance(relationship, ToManyContRelationship): + # Index remote object. It might have a custom path reporter. + notify(IndexingEvent(obj, 'path', False)) + + # For componentSearch. Would be nice if we could target + # Index remote object. It might have a custom path reporter. + notify(IndexingEvent(obj, 'path', False)) + + # For componentSearch. Would be nice if we could target + # idxs=['getAllPaths'], but there's a chance that it won't exist + # yet. + obj.index_object() + + +def addToMany(relationship, root, type_, id_): + ''' + Update ToMany relationship given search root, type and id. + + Adds a new ID to this relationship, without disturbing existing + objects. + ''' + root = root.primaryAq() + query = Eq('id', id_) + + for result in ICatalogTool(root).search(types=[type_], query=query): + obj = result.getObject() + relationship.addRelation(obj) + # Index remote object. It might have a custom path reporter. notify(IndexingEvent(obj, 'path', False)) @@ -73,6 +155,31 @@ def updateToMany(relationship, root, type_, ids): # idxs=['getAllPaths'], but there's a chance that it won't exist # yet. obj.index_object() + break + + +def removeToMany(relationship, root, type_, id_): + ''' + Update ToMany relationship given search root, type and id. + + Removes a single ID from this relationship, without disturbing + existing objects. + ''' + root = root.primaryAq() + query = Eq('id', id_) + + for result in ICatalogTool(root).search(types=[type_], query=query): + obj = result.getObject() + relationship.removeRelation() + + # Index remote object. It might have a custom path reporter. + notify(IndexingEvent(obj, 'path', False)) + + # For componentSearch. Would be nice if we could target + # idxs=['getAllPaths'], but there's a chance that it won't exist + # yet. + obj.index_object() + break def updateToOne(relationship, root, type_, id_): @@ -87,6 +194,7 @@ def updateToOne(relationship, root, type_, id_): # Return with no action if the relationship is already correct. if (old_obj and old_obj.id == id_) or (not old_obj and not id_): return + # Remove current object from relationship. if old_obj: relationship.removeRelation() @@ -113,8 +221,7 @@ def updateToOne(relationship, root, type_, id_): # idxs=['getAllPaths'], but there's a chance that it won't exist # yet. new_obj.index_object() - - return + break def RelationshipInfoProperty(relationship_name): @@ -146,22 +253,180 @@ def getter(self): return property(getter) -class BaseComponent(DeviceComponent, ManagedEntity): +class CatalogMixin(object): + ''' + Abstract class mixin to ease the creation and use of + component-specific catalogs. + + To use this mixin to create a component catalog you should define + a _catalog property such as the following on your mixed-in class:: + + _catalogs = dict({ + 'catalogName', { + 'deviceclass': '/Example/Device/Class', + 'indexes': { + 'ipv4_addresses': {'type': 'keyword'}, + 'mac_addresses': {'type': 'keyword'}, + }, + }, + }, **BaseClass._catalogs) + + The second item in each indexes tuple can either be keyword or + field. These correspond to Zope case-insensitive KeywordIndex and + FieldIndex. + ''' + + _catalogs = {} + + @classmethod + def _catalog_spec(cls, name): + spec = cls._catalogs.get(name) + if not spec: + LOG.error("%s catalog definition is missing", name) + return + + if not isinstance(spec, dict): + LOG.error("%s catalog definition is not a dict", name) + return + + if not spec.get('indexes'): + LOG.error("%s catalog definition has no indexes", name) + return + + if not spec.get('deviceclass'): + LOG.error("%s catalog definition has no deviceclass.", name) + return + + return spec + + @classmethod + def _create_catalog(cls, dmd, name): + from Products.ZCatalog.Catalog import CatalogError + from Products.ZCatalog.ZCatalog import manage_addZCatalog + + from Products.Zuul.interfaces import ICatalogTool + + spec = cls._catalog_spec(name) + if not spec: + return + + deviceclass = dmd.Devices.createOrganizer(spec['deviceclass']) + + if not hasattr(deviceclass, name): + manage_addZCatalog(deviceclass, name, name) + + zcatalog = deviceclass._getOb(name) + catalog = zcatalog._catalog + + for propname, propdata in spec['indexes'].items(): + index_type = propdata.get('type') + if not index_type: + LOG.error("%s index has no type", propname) + return + + index_factory = { + 'field': makeFieldIndex, + 'keyword': makeKeywordIndex, + }.get(index_type.lower()) + + if not index_factory: + LOG.error("%s is not a valid index type", index_type) + return + + try: + catalog.addIndex(propname, index_factory(propname)) + except CatalogError: + # Index already exists. + pass + else: + fqcn = '.'.join((cls.__module__, cls.__name__)) + results = ICatalogTool(dmd.primaryAq()).search(fqcn) + for brain in results: + brain.getObject().index_object() + + return zcatalog + + @classmethod + def _get_catalog(cls, dmd, name): + spec = cls._catalog_spec(name) + if not spec: + return + + deviceclass = dmd.Devices.createOrganizer(spec['deviceclass']) + + try: + return getattr(deviceclass, name) + except AttributeError: + return cls._create_catalog(dmd, name) + + @classmethod + def search(cls, dmd, name, **kwargs): + ''' + Generate instances of this object that match keyword arguments. + ''' + catalog = cls._get_catalog(dmd, name) + if not catalog: + return + + for brain in catalog(**kwargs): + yield brain.getObject() + + def index_object(self, idxs=None): + ''' + Index the mixed-in instance in its catalogs. + + We rely on subclasses to explicitely call this method in + addition to their primary inheritence index_object method as in + the following override:: + + def index_object(self, idxs=None): + for superclass in (ManagedEntity, CatalogMixin): + superclass.index_object(self, idxs=idxs) + ''' + for catalog in (self._get_catalog(self.dmd, x) for x in self._catalogs): + catalog.catalog_object(self, self.getPrimaryId()) + + def unindex_object(self): + ''' + Unindex the mixed-in instance from its catalogs. + + We rely on subclasses to explicitely call this method in + addition to their primary inheritence unindex_object method as + in the following override:: + + def unindex_object(self): + for superclass in (ManagedEntity, CatalogMixin): + superclass.unindex_object(self) + ''' + for catalog in (self._get_catalog(self.dmd, x) for x in self._catalogs): + catalog.uncatalog_object(self.getPrimaryId()) + + +class BaseComponent(DeviceComponent, ManagedEntity, CatalogMixin): ''' Abstract base class for components. ''' - xapi_ref = None - xapi_uuid = None + xenapi_ref = None + xenapi_uuid = None # Explicit inheritence. _properties = ManagedEntity._properties + ( - {'id': 'xapi_ref', 'type': 'string', 'mode': 'w'}, - {'id': 'xapi_uuid', 'type': 'string', 'mode': 'w'}, + {'id': 'xenapi_ref', 'type': 'string', 'mode': 'w'}, + {'id': 'xenapi_uuid', 'type': 'string', 'mode': 'w'}, ) _relations = ManagedEntity._relations + _catalogs = { + 'XenServerCatalog': { + 'deviceclass': '/XenServer', + 'indexes': { + 'xenapi_uuid': {'type': 'field'}, + }, + }, + } + factory_type_information = ({ 'actions': ({ 'id': 'perfConf', @@ -171,6 +436,14 @@ class BaseComponent(DeviceComponent, ManagedEntity): },), },) + @classmethod + def findByUUID(cls, dmd, xenapi_uuid): + ''' + Return the first XenServer component matching XenAPI uuid. + ''' + return next(cls.search( + dmd, 'XenServerCatalog', xenapi_uuid=xenapi_uuid), None) + def device(self): ''' Return device under which this component/device is contained. @@ -189,6 +462,20 @@ def device(self): 'while getting device for %s' % ( obj, exc, self)) + def index_object(self, idxs=None): + ''' + Index object according to ManagedEntity and CatalogMixin. + ''' + for superclass in (ManagedEntity, CatalogMixin): + superclass.index_object(self, idxs=idxs) + + def unindex_object(self): + ''' + Unindex object according to ManagedEntity and CatalogMixin. + ''' + for superclass in (ManagedEntity, CatalogMixin): + superclass.unindex_object(self) + def getRRDTemplateName(self): ''' Return name of monitoring template to bind to this component. @@ -211,6 +498,27 @@ def xenrrd_prefix(self): ''' return None + @classmethod + def objectmap(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI ref and properties. + + No generic implementation exists. Must be overridden in + subclasses that are modeled. + ''' + return None + + @classmethod + def objectmap_metrics(cls, ref, properties): + ''' + Return an ObjectMap given XenAPI ref and metrics properties. + + No generic implementation exists. Must be overridden in + subclasses that have properties modeled from their corresponding + _metrics class. + ''' + return None + class IBaseComponentInfo(IComponentInfo): ''' @@ -218,8 +526,8 @@ class IBaseComponentInfo(IComponentInfo): ''' endpoint = schema.Entity(title=_t('Endpoint')) - xapi_ref = schema.TextLine(title=_t(u'XenAPI Reference')) - xapi_uuid = schema.TextLine(title=_t(u'XenAPI UUID')) + xenapi_ref = schema.TextLine(title=_t(u'XenAPI Reference')) + xenapi_uuid = schema.TextLine(title=_t(u'XenAPI UUID')) class BaseComponentInfo(ComponentInfo): @@ -228,8 +536,8 @@ class BaseComponentInfo(ComponentInfo): ''' endpoint = RelationshipInfoProperty('device') - xapi_ref = ProxyProperty('xapi_ref') - xapi_uuid = ProxyProperty('xapi_uuid') + xenapi_ref = ProxyProperty('xenapi_ref') + xenapi_uuid = ProxyProperty('xenapi_uuid') class PooledComponent(BaseComponent): @@ -261,3 +569,76 @@ class PooledComponentInfo(BaseComponentInfo): ''' pool = RelationshipInfoProperty('pool') + + +def findIpInterfacesByMAC(dmd, macaddresses, interfaceType=None): + ''' + Yield IpInterface objects that match the parameters. + ''' + if not macaddresses: + return + + layer2_catalog = dmd.ZenLinkManager._getCatalog(layer=2) + if layer2_catalog is not None: + for result in layer2_catalog(macaddress=macaddresses): + iface = result.getObject() + if not interfaceType or isinstance(iface, interfaceType): + yield iface + + +def id_from_ref(ref): + ''' + Return a component id given a XenAPI OpaqueRef. + ''' + if not ref or ref == 'OpaqueRef:NULL': + return None + + return prepId(ref.split(':', 1)[1]) + + +def ids_from_refs(refs): + ''' + Return list of component ids given a list of XenAPI OpaqueRefs. + + Null references won't be included in the returned list. So it's + possible that the returned list will be shorter than the passed + list. + ''' + ids = [] + + for ref in refs: + id_ = id_from_ref(ref) + if id_: + ids.append(id_) + + return ids + + +def int_or_none(value): + ''' + Return value converted to int or None if conversion fails. + ''' + try: + return int(value) + except (TypeError, ValueError): + return None + + +def float_or_none(value): + ''' + Return value converted to float or None if conversion fails. + ''' + try: + return float(value) + except (TypeError, ValueError): + return None + + +def to_boolean(value, true_value='true'): + ''' + Return value converted to boolean. + ''' + if value == true_value: + return True + else: + return False diff --git a/impact.yuml b/impact.yuml new file mode 100644 index 0000000..4c9db00 --- /dev/null +++ b/impact.yuml @@ -0,0 +1,43 @@ +// XenServer Internal Impacts +[note: XenServer;ZenPack{bg:aquamarine}]-[Endpoint{bg:aquamarine}] +[Endpoint{bg:aquamarine}]1-1..*>[Host{bg:aquamarine}] +[Host{bg:aquamarine}]1..*-0..1>[Pool{bg:aquamarine}] +[Host{bg:aquamarine}]0..1-0..*>[VM{bg:aquamarine}] +[Pool{bg:aquamarine}]0..1-0..*>[VM{bg:aquamarine}] +[PBD{bg:aquamarine}]0..*-0..1>[SR{bg:aquamarine}] +[PBD{bg:aquamarine}]1..*-1>[Host{bg:aquamarine}] +[SR{bg:aquamarine}]1-0..*>[Host{bg:aquamarine}] +[SR{bg:aquamarine}]1-0..*>[Pool{bg:aquamarine}] +[SR{bg:aquamarine}]1-0..*>[VDI{bg:aquamarine}] +[VDI{bg:aquamarine}]0..1-0..*>[VBD{bg:aquamarine}] +[VBD{bg:aquamarine}]1..*-1>[VM{bg:aquamarine}] +[PIF{bg:aquamarine}]0..*-0..1[Network{bg:aquamarine}] +[PIF{bg:aquamarine}]1..*-1>[Host{bg:aquamarine}] +[Network{bg:aquamarine}]1-0..*>[VIF{bg:aquamarine}] +[VIF{bg:aquamarine}]1..*-1>[VM{bg:aquamarine}] +[VM{bg:aquamarine}]1..*-0..1>[vApp{bg:aquamarine}] +// +// Platform Impacts +[note: Platform;(physical){bg:deepskyblue}]-[Device;(physical){bg:deepskyblue}] +[note: Platform;(physical){bg:deepskyblue}]-[IpInterface;(physical){bg:deepskyblue}] +[note: Platform;(physical){bg:deepskyblue}]-[HardDisk;(physical){bg:deepskyblue}] +[Device;(physical){bg:deepskyblue}]0..1-1>[Host] +[IpInterface;(physical){bg:deepskyblue}]0..1-1>[PIF] +[HardDisk;(physical){bg:deepskyblue}]0..1-1>[PBD] +// +[note: Platform;(virtual){bg:deepskyblue}]-[Device;(virtual){bg:deepskyblue}] +[note: Platform;(virtual){bg:deepskyblue}]-[IpInterface;(virtual){bg:deepskyblue}] +[note: Platform;(virtual){bg:deepskyblue}]-[HardDisk;(virtual){bg:deepskyblue}] +[VM]1-0..1>[Device;(virtual){bg:deepskyblue}] +[VIF]1-0..1>[IpInterface;(virtual){bg:deepskyblue}] +[VBD]1-0..1>[HardDisk;(virtual){bg:deepskyblue}] +// +// CloudStack Impacts +[note: CloudStack{bg:cadetblue}]-[CloudStack Host{bg:cadetblue}] +[note: CloudStack{bg:cadetblue}]-[CloudStack VM{bg:cadetblue}] +[note: CloudStack{bg:cadetblue}]-[SystemVM{bg:cadetblue}] +[note: CloudStack{bg:cadetblue}]-[RouterVM{bg:cadetblue}] +[Host{bg:aquamarine}]1-0..1>[CloudStack Host{bg:cadetblue}] +[VM{bg:aquamarine}]1-0..1>[CloudStack VM{bg:cadetblue}] +[VM{bg:aquamarine}]1-0..1>[SystemVM{bg:cadetblue}] +[VM{bg:aquamarine}]1-0..1>[RouterVM{bg:cadetblue}] diff --git a/model.yuml b/model.yuml index 9967fb5..d4564eb 100644 --- a/model.yuml +++ b/model.yuml @@ -1,5 +1,3 @@ -[note: XenServer Model ERD{bg:cornsilk}] -// [Endpoint]<>1-0..1[Pool] [Endpoint]<>1-0..*[SR] [Endpoint]<>1-0..*[Host] diff --git a/monitoring_templates.yaml b/monitoring_templates.yaml index 3d4e9c6..97b7aed 100644 --- a/monitoring_templates.yaml +++ b/monitoring_templates.yaml @@ -1,11 +1,30 @@ +/XenServer/Endpoint: + targetPythonClass: "ZenPacks.zenoss.XenServer.Endpoint" + + datasources: + events: + type: Python + plugin_classname: "ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIEventsPlugin" + eventClass: "/Ignore" + severity: "0" + cycletime: "${here/zXenServerModelInterval}" + + messages: + type: Python + plugin_classname: "ZenPacks.zenoss.XenServer.datasource_plugins.XenAPIMessagesPlugin" + eventClass: "/Ignore" + severity: "0" + cycletime: "${here/zXenServerEventsInterval}" + + /XenServer/Host: targetPythonClass: "ZenPacks.zenoss.XenServer.Host" datasources: property: - type: XenServer XAPI - xapi_classname: host - xapi_ref: "${here/xapi_ref}" + type: XenServer XenAPI + xenapi_classname: host + xenapi_ref: "${here/xenapi_ref}" datapoints: memoryOverhead: @@ -211,9 +230,9 @@ datasources: metric: - type: XenServer XAPI - xapi_classname: PIF_metrics - xapi_ref: "${here/xapi_metrics_ref}" + type: XenServer XenAPI + xenapi_classname: PIF_metrics + xenapi_ref: "${here/xenapi_metrics_ref}" datapoints: speed: @@ -243,22 +262,12 @@ format: "%7.2lf%s" color: "000000" - Receive_m: - dpName: "metric_ioRead" - format: "%7.2lf%s" - colorindex: 0 - - Send_m: - dpName: "metric_ioWrite" - format: "%7.2lf%s" - colorindex: 1 - - Receive_r: + Receive: dpName: "rrd_rx" format: "%7.2lf%s" colorindex: 0 - Send_r: + Send: dpName: "rrd_tx" format: "%7.2lf%s" colorindex: 1 @@ -269,9 +278,9 @@ datasources: property: - type: XenServer XAPI - xapi_classname: pool - xapi_ref: "${here/xapi_ref}" + type: XenServer XenAPI + xenapi_classname: pool + xenapi_ref: "${here/xenapi_ref}" datapoints: haPlanExistsFor: @@ -302,9 +311,9 @@ datasources: property: - type: XenServer XAPI - xapi_classname: SR - xapi_ref: "${here/xapi_ref}" + type: XenServer XenAPI + xenapi_classname: SR + xenapi_ref: "${here/xenapi_ref}" datapoints: physicalSize: @@ -377,9 +386,9 @@ datasources: property: - type: XenServer XAPI - xapi_classname: VDI - xapi_ref: "${here/xapi_ref}" + type: XenServer XenAPI + xenapi_classname: VDI + xenapi_ref: "${here/xenapi_ref}" datapoints: physicalUtilisation: @@ -404,6 +413,7 @@ Physical: dpName: "property_physicalUtilisation" + lineType: AREA colorindex: 0 format: "%7.2lf%s" @@ -445,18 +455,18 @@ datasources: property: - type: XenServer XAPI - xapi_classname: VM - xapi_ref: "${here/xapi_ref}" + type: XenServer XenAPI + xenapi_classname: VM + xenapi_ref: "${here/xenapi_ref}" datapoints: memoryOverhead: path: memory_overhead metric: - type: XenServer XAPI - xapi_classname: VM_metrics - xapi_ref: "${here/xapi_metrics_ref}" + type: XenServer XenAPI + xenapi_classname: VM_metrics + xenapi_ref: "${here/xenapi_metrics_ref}" datapoints: vcpusNumber: @@ -582,5 +592,9 @@ format: "%7.2lf%s" +/XenServer/VMAppliance: + targetPythonClass: "ZenPacks.zenoss.XenServer.VMAppliance" + + /XenServer/VMGuest: targetPythonClass: "ZenPacks.zenoss.XenServer.VM" diff --git a/screenshots/xenserver_add_dialog.png b/screenshots/xenserver_add_dialog.png new file mode 100644 index 0000000..15f86f4 Binary files /dev/null and b/screenshots/xenserver_add_dialog.png differ diff --git a/screenshots/xenserver_add_menu.png b/screenshots/xenserver_add_menu.png new file mode 100644 index 0000000..e3605f9 Binary files /dev/null and b/screenshots/xenserver_add_menu.png differ diff --git a/screenshots/xenserver_events.png b/screenshots/xenserver_events.png new file mode 100644 index 0000000..6fb2359 Binary files /dev/null and b/screenshots/xenserver_events.png differ diff --git a/screenshots/xenserver_hostcpus.png b/screenshots/xenserver_hostcpus.png new file mode 100644 index 0000000..bb4713d Binary files /dev/null and b/screenshots/xenserver_hostcpus.png differ diff --git a/screenshots/xenserver_hosts.png b/screenshots/xenserver_hosts.png new file mode 100644 index 0000000..d50f7eb Binary files /dev/null and b/screenshots/xenserver_hosts.png differ diff --git a/screenshots/xenserver_impact.png b/screenshots/xenserver_impact.png new file mode 100644 index 0000000..ec4063d Binary files /dev/null and b/screenshots/xenserver_impact.png differ diff --git a/screenshots/xenserver_impact_yuml.png b/screenshots/xenserver_impact_yuml.png new file mode 100644 index 0000000..5340478 Binary files /dev/null and b/screenshots/xenserver_impact_yuml.png differ diff --git a/screenshots/xenserver_model_yuml.png b/screenshots/xenserver_model_yuml.png new file mode 100644 index 0000000..d1d6625 Binary files /dev/null and b/screenshots/xenserver_model_yuml.png differ diff --git a/screenshots/xenserver_networks.png b/screenshots/xenserver_networks.png new file mode 100644 index 0000000..3e2ce25 Binary files /dev/null and b/screenshots/xenserver_networks.png differ diff --git a/screenshots/xenserver_pbds.png b/screenshots/xenserver_pbds.png new file mode 100644 index 0000000..e7f9ac5 Binary files /dev/null and b/screenshots/xenserver_pbds.png differ diff --git a/screenshots/xenserver_pifs.png b/screenshots/xenserver_pifs.png new file mode 100644 index 0000000..ec226d0 Binary files /dev/null and b/screenshots/xenserver_pifs.png differ diff --git a/screenshots/xenserver_pools.png b/screenshots/xenserver_pools.png new file mode 100644 index 0000000..0368bba Binary files /dev/null and b/screenshots/xenserver_pools.png differ diff --git a/screenshots/xenserver_srs.png b/screenshots/xenserver_srs.png new file mode 100644 index 0000000..9726f31 Binary files /dev/null and b/screenshots/xenserver_srs.png differ diff --git a/screenshots/xenserver_vapps.png b/screenshots/xenserver_vapps.png new file mode 100644 index 0000000..4b4b8c7 Binary files /dev/null and b/screenshots/xenserver_vapps.png differ diff --git a/screenshots/xenserver_vbds.png b/screenshots/xenserver_vbds.png new file mode 100644 index 0000000..06ddcd3 Binary files /dev/null and b/screenshots/xenserver_vbds.png differ diff --git a/screenshots/xenserver_vdis.png b/screenshots/xenserver_vdis.png new file mode 100644 index 0000000..ec73ba1 Binary files /dev/null and b/screenshots/xenserver_vdis.png differ diff --git a/screenshots/xenserver_vifs.png b/screenshots/xenserver_vifs.png new file mode 100644 index 0000000..b0b08b3 Binary files /dev/null and b/screenshots/xenserver_vifs.png differ diff --git a/screenshots/xenserver_vms.png b/screenshots/xenserver_vms.png new file mode 100644 index 0000000..fe127f4 Binary files /dev/null and b/screenshots/xenserver_vms.png differ diff --git a/setup.py b/setup.py index 83e7b95..b107598 100644 --- a/setup.py +++ b/setup.py @@ -2,12 +2,12 @@ # or saved. Do not modify them directly here. # NB: PACKAGES is deprecated NAME = "ZenPacks.zenoss.XenServer" -VERSION = "1.0.0dev2" +VERSION = "1.0.0beta1" AUTHOR = "Zenoss" LICENSE = "GPLv2" NAMESPACE_PACKAGES = ['ZenPacks', 'ZenPacks.zenoss'] PACKAGES = ['ZenPacks', 'ZenPacks.zenoss', 'ZenPacks.zenoss.XenServer'] -INSTALL_REQUIRES = ['ZenPacks.zenoss.PythonCollector'] +INSTALL_REQUIRES = ['ZenPacks.zenoss.PythonCollector>=1.1'] COMPAT_ZENOSS_VERS = ">=4.2" PREV_ZENPACK_NAME = "" # STOP_REPLACEMENTS