Skip to content

Commit

Permalink
Initial commit (with following squashed)
Browse files Browse the repository at this point in the history
Clean up config options

Add dependency

Re-implement excluded_host_tags work

Fix tests

Fetch remaining metrics

Clean-up

Fix dependencies

fix style

[review] max_query_metrics exception

[review] Document self._conn.content type

[review] Address minor comments
  • Loading branch information
FlorianVeaux committed Jan 8, 2020
1 parent f72a9fd commit bcd4714
Show file tree
Hide file tree
Showing 42 changed files with 57,785 additions and 1,157 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ddtrace==0.13.0
dnspython==1.16.0
flup==1.0.3.dev-20110405; python_version < '3.0'
flup-py3==1.0.3; python_version > '3.0'
futures==3.3.0; python_version < '3.0'
gearman==2.0.2; sys_platform != 'win32' and python_version < '3.0'
httplib2==0.10.3
in-toto==0.4.1
Expand Down
3 changes: 3 additions & 0 deletions vsphere/datadog_checks/vsphere/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# (C) Datadog, Inc. 2019
# All rights reserved
# Licensed under Simplified BSD License (see LICENSE)
from .__about__ import __version__
from .vsphere import VSphereCheck

Expand Down
175 changes: 175 additions & 0 deletions vsphere/datadog_checks/vsphere/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# (C) Datadog, Inc. 2019
# All rights reserved
# Licensed under Simplified BSD License (see LICENSE)
import functools
import ssl

from pyVim import connect
from pyVmomi import vim, vmodl # pylint: disable=E0611

from datadog_checks.base import ensure_unicode, is_affirmative
from datadog_checks.vsphere.constants import (
ALL_RESOURCES,
DEFAULT_BATCH_COLLECTOR_SIZE,
MAX_QUERY_METRICS_OPTION,
UNLIMITED_HIST_METRICS_PER_QUERY,
)


def smart_retry(f):
"""A function decorated with this `@smart_retry` will trigger a new authentication if it fails. The function
will then be retried.
This is useful when the integration keeps a semi-healthy connection to the vSphere API"""

@functools.wraps(f)
def wrapper(*args, **kwargs):
api_instance = args[0]
try:
return f(*args, **kwargs)
except Exception:
api_instance.smart_connect()
return f(*args, **kwargs)

return wrapper


class APIConnectionError(Exception):
pass


class VSphereAPI(object):
"""Abstraction class over the vSphere SOAP api using the pyvmomi library"""

def __init__(self, instance):
self.host = instance['host']
self.username = instance['username']
self.password = instance['password']
self.ssl_verify = is_affirmative(instance.get('ssl_verify', True))
self.ssl_capath = instance.get('ssl_capath')
self.batch_collector_size = instance.get('batch_property_collector_size', DEFAULT_BATCH_COLLECTOR_SIZE)
self._conn = None
self.smart_connect()

def smart_connect(self):
"""Creates the connection object to the vSphere API using parameters supplied from the configuration.
"""
context = None
if not self.ssl_verify:
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.verify_mode = ssl.CERT_NONE
elif self.ssl_capath:
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(capath=self.ssl_capath)

try:
# Object returned by SmartConnect is a ServerInstance
# https://www.vmware.com/support/developer/vc-sdk/visdk2xpubs/ReferenceGuide/vim.ServiceInstance.html
conn = connect.SmartConnect(host=self.host, user=self.username, pwd=self.password, sslContext=context)
conn.CurrentTime()
except Exception as e:
err_msg = "Connection to {} failed: {}".format(ensure_unicode(self.host), e)
raise APIConnectionError(err_msg)

self._conn = conn

@smart_retry
def check_health(self):
self._conn.CurrentTime()

@smart_retry
def get_perf_counter_by_level(self, collection_level):
"""Requests and returns the list of counter available for a given collection_level."""
return self._conn.content.perfManager.QueryPerfCounterByLevel(collection_level)

@smart_retry
def get_infrastructure(self):
"""Traverse the whole vSphere infrastructure and outputs a dict mapping the mors to their properties.
:return: {
'vim.VirtualMachine-VM0': {
'name': 'VM-0',
...
}
...
}
"""
content = self._conn.content # vim.ServiceInstanceContent reference from the connection

property_specs = []
# Specify which attributes we want to retrieve per object
for resource in ALL_RESOURCES:
property_spec = vmodl.query.PropertyCollector.PropertySpec()
property_spec.type = resource
property_spec.pathSet = ["name", "parent", "customValue"]
if resource == vim.VirtualMachine:
property_spec.pathSet.append("runtime.powerState")
property_spec.pathSet.append("runtime.host")
property_spec.pathSet.append("guest.hostName")
property_specs.append(property_spec)

# Specify the attribute of the root object to traverse to obtain all the attributes
traversal_spec = vmodl.query.PropertyCollector.TraversalSpec()
traversal_spec.path = "view"
traversal_spec.skip = False
traversal_spec.type = vim.view.ContainerView

retr_opts = vmodl.query.PropertyCollector.RetrieveOptions()
# To limit the number of objects retrieved per call.
# If batch_collector_size is 0, collect maximum number of objects.
retr_opts.maxObjects = self.batch_collector_size

# Specify the root object from where we collect the rest of the objects
obj_spec = vmodl.query.PropertyCollector.ObjectSpec()
obj_spec.skip = True
obj_spec.selectSet = [traversal_spec]

# Create our filter spec from the above specs
filter_spec = vmodl.query.PropertyCollector.FilterSpec()
filter_spec.propSet = property_specs

view_ref = content.viewManager.CreateContainerView(content.rootFolder, ALL_RESOURCES, True)
try:
obj_spec.obj = view_ref
filter_spec.objectSet = [obj_spec]

# Collect the objects and their properties
res = content.propertyCollector.RetrievePropertiesEx([filter_spec], retr_opts)
mors = res.objects
# Results can be paginated
while res.token is not None:
res = content.propertyCollector.ContinueRetrievePropertiesEx(res.token)
mors.extend(res.objects)
finally:
view_ref.Destroy()

infrastructure_data = {mor.obj: {prop.name: prop.val for prop in mor.propSet} for mor in mors if mor.propSet}

root_folder = self._conn.content.rootFolder
infrastructure_data[root_folder] = {"name": root_folder.name, "parent": None}
return infrastructure_data

@smart_retry
def query_metrics(self, query_specs):
perf_manager = self._conn.content.perfManager
values = perf_manager.QueryPerf(query_specs)
return values

@smart_retry
def get_new_events(self, start_time):
event_manager = self._conn.content.eventManager
query_filter = vim.event.EventFilterSpec()
time_filter = vim.event.EventFilterSpec.ByTime(beginTime=start_time)
query_filter.time = time_filter
return event_manager.QueryEvents(query_filter)

@smart_retry
def get_latest_event_timestamp(self):
event_manager = self._conn.content.eventManager
return event_manager.latestEvent.createdTime

@smart_retry
def get_max_query_metrics(self):
vcenter_settings = self._conn.content.setting.QueryOptions(MAX_QUERY_METRICS_OPTION)
max_historical_metrics = int(vcenter_settings[0].value)
return max_historical_metrics if max_historical_metrics > 0 else UNLIMITED_HIST_METRICS_PER_QUERY
91 changes: 91 additions & 0 deletions vsphere/datadog_checks/vsphere/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# (C) Datadog, Inc. 2019
# All rights reserved
# Licensed under Simplified BSD License (see LICENSE)
import time
from contextlib import contextmanager


class VSphereCache(object):
"""
Wraps configuration and status for the Morlist and Metadata caches.
VSphereCache is *not* threadsafe.
"""

def __init__(self, interval_sec, log=None):
self._last_ts = 0
self._interval = interval_sec
self._content = {}
self.log = log

@contextmanager
def update(self):
"""A context manager to allow modification of the cache. It will restore the previous value
on any error.
Usage:
```
with cache.update():
cache.set_XXX(SOME_DATA)
```
"""
old_content = self._content
self._content = {} # 1. clear the content
try:
yield # 2. Actually update the cache
self._last_ts = time.time() # 3. Cache was updated successfully
except Exception:
# Restore old data
self._content = old_content
raise

def is_expired(self):
"""The cache has a global time to live, all elements expire at the same time.
:return True if the cache is expired."""
elapsed = time.time() - self._last_ts
return elapsed > self._interval


class MetricsMetadataCache(VSphereCache):
"""A VSphere cache dedicated to store the metrics metadata from a user environment.
Data is stored like this:
_content = {
vim.HostSystem: {
<COUNTER_KEY>: <DD_METRIC_NAME>,
...
},
vim.VirtualMachine: {...},
...
}
"""

def get_metadata(self, resource_type):
return self._content.get(resource_type)

def set_metadata(self, resource_type, metadata):
self._content[resource_type] = metadata


class InfrastructureCache(VSphereCache):
"""A VSphere cache dedicated to store the infrastructure data from a user environment.
Data is stored like this:
_content = {
vim.VirtualMachine: {
<MOR_REFERENCE>: <MOR_PROPS_DICT>
},
...
}
"""

def get_mor_props(self, mor, default=None):
mor_type = type(mor)
return self._content.get(mor_type, {}).get(mor, default)

def get_mors(self, resource_type):
return self._content.get(resource_type, {}).keys()

def set_mor_data(self, mor, mor_data):
mor_type = type(mor)
if mor_type not in self._content:
self._content[mor_type] = {}
self._content[mor_type][mor] = mor_data
45 changes: 45 additions & 0 deletions vsphere/datadog_checks/vsphere/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# (C) Datadog, Inc. 2019
# All rights reserved
# Licensed under Simplified BSD License (see LICENSE)
from pyVmomi import vim

ALLOWED_FILTER_PROPERTIES = ['name', 'inventory_path']
EXTRA_FILTER_PROPERTIES_FOR_VMS = ['hostname', 'guest_hostname']
SHORT_ROLLUP = {
"average": "avg",
"summation": "sum",
"maximum": "max",
"minimum": "min",
"latest": "latest",
"none": "raw",
}

MOR_TYPE_AS_STRING = {
vim.HostSystem: 'host',
vim.VirtualMachine: 'vm',
vim.Datacenter: 'datacenter',
vim.Datastore: 'datastore',
vim.ClusterComputeResource: 'cluster',
}

ALL_RESOURCES = [
vim.VirtualMachine,
vim.HostSystem,
vim.Datacenter,
vim.Datastore,
vim.ClusterComputeResource,
vim.ComputeResource,
vim.Folder,
]
REALTIME_RESOURCES = [vim.VirtualMachine, vim.HostSystem]
HISTORICAL_RESOURCES = [vim.Datacenter, vim.Datastore, vim.ClusterComputeResource]
ALL_RESOURCES_WITH_METRICS = REALTIME_RESOURCES + HISTORICAL_RESOURCES

REALTIME_METRICS_INTERVAL_ID = 20

DEFAULT_BATCH_COLLECTOR_SIZE = 500
DEFAULT_METRICS_PER_QUERY = 500
UNLIMITED_HIST_METRICS_PER_QUERY = float('inf')
DEFAULT_MAX_QUERY_METRICS = 256
MAX_QUERY_METRICS_OPTION = "config.vpxd.stats.maxQueryMetrics"
DEFAULT_THREAD_COUNT = 4
Loading

0 comments on commit bcd4714

Please sign in to comment.