monasca-agent/monasca_setup/detection/plugins/libvirt.py

252 lines
10 KiB
Python

# (c) Copyright 2015-2016 Hewlett Packard Enterprise Development LP
# Copyright 2017 Fujitsu LIMITED
# Copyright 2017 SUSE Linux GmbH
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import os
import pwd
from shutil import copy
import subprocess
import sys
from oslo_config import cfg
from oslo_utils import importutils
from monasca_setup import agent_config
from monasca_setup.detection import plugin
from monasca_setup.detection import utils
log = logging.getLogger(__name__)
# Directory to use for instance and metric caches (preferred tmpfs "/dev/shm")
cache_dir = "/dev/shm"
# Maximum age of instance cache before automatic refresh (in seconds)
nova_refresh = 60 * 60 * 4 # Four hours
# Probation period before metrics are gathered for a VM (in seconds)
vm_probation = 60 * 5 # Five minutes
# List of instance metadata keys to be sent as dimensions
# By default 'scale_group' metadata is used here for supporting auto
# scaling in Heat.
metadata = ['scale_group']
# Include scale group dimension for customer metrics.
customer_metadata = ['scale_group']
# List 'ping' commands (paths and parameters) in order of preference.
# The plugin will use the first functional command. 127.0.0.1 will be appended.
ping_options = [["/usr/bin/fping", "-n", "-c1", "-t250", "-q"],
["/sbin/fping", "-n", "-c1", "-t250", "-q"],
["/bin/ping", "-n", "-c1", "-w1", "-q"],
["/usr/bin/ping", "-n", "-c1", "-w1", "-q"]]
# Path to 'ip' command (needed to execute ping within network namespaces)
ip_cmd = "/sbin/ip"
# How many ping commands to run concurrently
default_max_ping_concurrency = 8
# Disk metrics can be collected at a larger interval than other vm metrics
default_disk_collection_period = 0
# VNIC metrics can be collected at a larger interval than other vm metrics
default_vnic_collection_period = 0
# Arguments which should be written as integers, not strings
INT_ARGS = ['disk_collection_period', 'vnic_collection_period',
'max_ping_concurrency', 'nova_refresh', 'vm_probation']
_REQUIRED_OPTS = [
{'opt': cfg.StrOpt('username'), 'group': 'keystone_authtoken'},
{'opt': cfg.StrOpt('user_domain_name'), 'group': 'keystone_authtoken'},
{'opt': cfg.StrOpt('password'), 'group': 'keystone_authtoken'},
{'opt': cfg.StrOpt('project_name'), 'group': 'keystone_authtoken'},
{'opt': cfg.StrOpt('project_domain_name'), 'group': 'keystone_authtoken'},
{'opt': cfg.StrOpt('auth_url'), 'group': 'keystone_authtoken'}
]
"""Nova configuration opts required by this plugin"""
class Libvirt(plugin.Plugin):
"""Configures VM monitoring through Nova"""
FAILED_DETECTION_MSG = 'libvirt plugin will not not be configured.'
def _detect(self):
"""Set self.available True if the process and config file are detected
"""
# NOTE(trebskit) bind each check we execute to another one
# that way if X-one fails following won't be executed
# and detection phase will end faster
nova_proc = utils.find_process_name('nova-compute')
has_deps = self.dependencies_installed() if nova_proc else None
nova_conf = self._find_nova_conf(nova_proc) if has_deps else None
has_cache_dir = self._has_cache_dir() if nova_conf else None
agent_user = utils.get_agent_username() if has_cache_dir else None
self.available = nova_conf and has_cache_dir
if not self.available:
if not nova_proc:
detailed_message = '\tnova-compute process not found.'
log.info('%s\n%s' % (detailed_message,
self.FAILED_DETECTION_MSG))
elif not has_deps:
detailed_message = ('\tRequired dependencies were not found.\n'
'Run pip install monasca-agent[libvirt] '
'to install all dependencies.')
log.warning('%s\n%s' % (detailed_message,
self.FAILED_DETECTION_MSG))
elif not has_cache_dir:
detailed_message = '\tCache directory %s not found' % cache_dir
log.warning('%s\n%s' % (detailed_message,
self.FAILED_DETECTION_MSG))
elif not nova_conf:
detailed_message = ('\tnova-compute process was found, '
'but it was impossible to '
'read it\'s configuration.')
log.warning('%s\n%s' % (detailed_message,
self.FAILED_DETECTION_MSG))
else:
self.nova_conf = nova_conf
self._agent_user = agent_user
def build_config(self):
"""Build the config as a Plugins object and return back.
"""
config = agent_config.Plugins()
init_config = self._get_init_config()
self._configure_ping(init_config)
# Handle monasca-setup detection arguments, which take precedence
if self.args:
for arg in self.args:
if arg in INT_ARGS:
value = self.args[arg]
try:
init_config[arg] = int(value)
except ValueError:
log.warn("\tInvalid integer value '{0}' for parameter {1}, ignoring value"
.format(value, arg))
else:
init_config[arg] = self.literal_eval(self.args[arg])
config['libvirt'] = {'init_config': init_config,
'instances': []}
return config
def _configure_ping(self, init_config):
if self._agent_user is None:
log.warn("\tUnable to determine agent user. Skipping ping checks.")
return
try:
client = importutils.try_import('neutronclient.v2_0.client',
False)
if not client:
log.warning(
'\tpython-neutronclient module missing, '
'required for ping checks.')
return
# TODO(dmllr) Find a better rundir or avoid copying the binary
# alltogether. see https://storyboard.openstack.org/#!/story/2001593
monasca_rundir = sys.path[0]
monasca_ip = "{0}/monasca-agent-ip".format(monasca_rundir)
# Copy system 'ip' command to monasca_rundir
copy(ip_cmd, monasca_ip)
# Restrict permissions on the local 'ip' command
os.chown(monasca_ip, *self._get_user_uid_gid(self._agent_user))
os.chmod(monasca_ip, 0o700)
# Set capabilities on 'ip' which will allow
# self.agent_user to exec commands in namespaces
setcap_cmd = ['/sbin/setcap', 'cap_sys_admin+ep',
monasca_ip]
subprocess.Popen(setcap_cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# Verify that the capabilities were set
setcap_cmd.extend(['-v', '-q'])
subprocess.check_call(setcap_cmd)
# Look for the best ping command
for ping_cmd in ping_options:
if os.path.isfile(ping_cmd[0]):
init_config[
'ping_check'] = "{0} netns exec NAMESPACE {1}".format(
monasca_ip,
' '.join(ping_cmd))
log.info(
"\tEnabling ping checks using {0}".format(ping_cmd[0]))
init_config['ping_check'] = True
break
if init_config['ping_check'] is False:
log.warn('\tUnable to find suitable ping command, '
'disabling ping checks.')
except IOError:
log.warn('\tUnable to copy {0}, '
'ping checks disabled.'.format(ip_cmd))
pass
except (subprocess.CalledProcessError, OSError):
log.warn('\tUnable to set up ping checks, '
'setcap failed ({0})'.format(' '.join(setcap_cmd)))
pass
def dependencies_installed(self):
return importutils.try_import('novaclient.client', False)
def _get_init_config(self):
keystone_auth_section = self.nova_conf['keystone_authtoken']
init_config = {
'cache_dir': cache_dir,
'nova_refresh': nova_refresh,
'metadata': metadata,
'vm_probation': vm_probation,
'customer_metadata': customer_metadata,
'max_ping_concurrency': default_max_ping_concurrency,
'disk_collection_period': default_disk_collection_period,
'vnic_collection_period': default_vnic_collection_period,
'vm_cpu_check_enable': True,
'vm_disks_check_enable': True,
'vm_network_check_enable': True,
'vm_ping_check_enable': True,
'vm_extended_disks_check_enable': False,
'ping_check': False,
'username': keystone_auth_section['username'],
'user_domain_name': keystone_auth_section['user_domain_name'],
'password': keystone_auth_section['password'],
'project_name': keystone_auth_section['project_name'],
'project_domain_name': keystone_auth_section['project_domain_name'],
'auth_url': keystone_auth_section['auth_url']
}
return init_config
@staticmethod
def _has_cache_dir():
return os.path.isdir(cache_dir)
@staticmethod
def _find_nova_conf(nova_process):
try:
nova_cmd = nova_process.as_dict(['cmdline'])['cmdline']
return utils.load_oslo_configuration(from_cmd=nova_cmd,
in_project='nova',
for_opts=_REQUIRED_OPTS)
except cfg.Error:
log.exception('Failed to load nova configuration')
return None
@staticmethod
def _get_user_uid_gid(username):
stat = pwd.getpwnam(username)
uid = stat.pw_uid
gid = stat.pw_gid
return uid, gid