nova/nova/notifications.py

364 lines
12 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 OpenStack Foundation
# All Rights Reserved.
#
# 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.
"""Functionality related to notifications common to multiple layers of
the system.
"""
from oslo.config import cfg
from nova.compute import instance_types
import nova.context
from nova import db
from nova.image import glance
from nova import network
from nova.network import model as network_model
from nova.openstack.common import excutils
from nova.openstack.common import log
from nova.openstack.common.notifier import api as notifier_api
from nova.openstack.common import timeutils
from nova import utils
LOG = log.getLogger(__name__)
notify_opts = [
cfg.StrOpt('notify_on_state_change', default=None,
help='If set, send compute.instance.update notifications on instance '
'state changes. Valid values are None for no notifications, '
'"vm_state" for notifications on VM state changes, or '
'"vm_and_task_state" for notifications on VM and task state '
'changes.'),
cfg.BoolOpt('notify_on_any_change', default=False,
help='If set, send compute.instance.update notifications on instance '
'state changes. Valid values are False for no notifications, '
'True for notifications on any instance changes.'),
cfg.BoolOpt('notify_api_faults', default=False,
help='If set, send api.fault notifications on caught exceptions '
'in the API service.'),
]
CONF = cfg.CONF
CONF.register_opts(notify_opts)
def send_api_fault(url, status, exception):
"""Send an api.fault notification."""
if not CONF.notify_api_faults:
return
payload = {'url': url, 'exception': str(exception), 'status': status}
publisher_id = notifier_api.publisher_id("api")
notifier_api.notify(None, publisher_id, 'api.fault', notifier_api.ERROR,
payload)
def send_update(context, old_instance, new_instance, service=None, host=None):
"""Send compute.instance.update notification to report any changes occurred
in that instance
"""
if not CONF.notify_on_any_change and not CONF.notify_on_state_change:
# skip all this if updates are disabled
return
update_with_state_change = False
old_vm_state = old_instance["vm_state"]
new_vm_state = new_instance["vm_state"]
old_task_state = old_instance["task_state"]
new_task_state = new_instance["task_state"]
# we should check if we need to send a state change or a regular
# notification
if old_vm_state != new_vm_state:
# yes, the vm state is changing:
update_with_state_change = True
elif CONF.notify_on_state_change:
if (CONF.notify_on_state_change.lower() == "vm_and_task_state" and
old_task_state != new_task_state):
# yes, the task state is changing:
update_with_state_change = True
if update_with_state_change:
# send a notification with state changes
# value of verify_states need not be True as the check for states is
# already done here
send_update_with_states(context, new_instance, old_vm_state,
new_vm_state, old_task_state, new_task_state, service, host)
else:
try:
_send_instance_update_notification(context, new_instance,
service=service, host=host)
except Exception:
LOG.exception(_("Failed to send state update notification"),
instance=new_instance)
def send_update_with_states(context, instance, old_vm_state, new_vm_state,
old_task_state, new_task_state, service="compute", host=None,
verify_states=False):
"""Send compute.instance.update notification to report changes if there
are any, in the instance
"""
if not CONF.notify_on_state_change:
# skip all this if updates are disabled
return
fire_update = True
# send update notification by default
if verify_states:
# check whether we need to send notification related to state changes
fire_update = False
# do not send notification if the conditions for vm and(or) task state
# are not satisfied
if old_vm_state != new_vm_state:
# yes, the vm state is changing:
fire_update = True
elif CONF.notify_on_state_change:
if (CONF.notify_on_state_change.lower() == "vm_and_task_state" and
old_task_state != new_task_state):
# yes, the task state is changing:
fire_update = True
if fire_update:
# send either a state change or a regular notification
try:
_send_instance_update_notification(context, instance,
old_vm_state=old_vm_state, old_task_state=old_task_state,
new_vm_state=new_vm_state, new_task_state=new_task_state,
service=service, host=host)
except Exception:
LOG.exception(_("Failed to send state update notification"),
instance=instance)
def _send_instance_update_notification(context, instance, old_vm_state=None,
old_task_state=None, new_vm_state=None, new_task_state=None,
service="compute", host=None):
"""Send 'compute.instance.update' notification to inform observers
about instance state changes"""
payload = info_from_instance(context, instance, None, None)
if not new_vm_state:
new_vm_state = instance["vm_state"]
if not new_task_state:
new_task_state = instance["task_state"]
states_payload = {
"old_state": old_vm_state,
"state": new_vm_state,
"old_task_state": old_task_state,
"new_task_state": new_task_state,
}
payload.update(states_payload)
# add audit fields:
(audit_start, audit_end) = audit_period_bounds(current_period=True)
payload["audit_period_beginning"] = audit_start
payload["audit_period_ending"] = audit_end
# add bw usage info:
bw = bandwidth_usage(instance, audit_start)
payload["bandwidth"] = bw
publisher_id = notifier_api.publisher_id(service, host)
notifier_api.notify(context, publisher_id, 'compute.instance.update',
notifier_api.INFO, payload)
def audit_period_bounds(current_period=False):
"""Get the start and end of the relevant audit usage period
:param current_period: if True, this will generate a usage for the
current usage period; if False, this will generate a usage for the
previous audit period.
"""
begin, end = utils.last_completed_audit_period()
if current_period:
audit_start = end
audit_end = timeutils.utcnow()
else:
audit_start = begin
audit_end = end
return (audit_start, audit_end)
def bandwidth_usage(instance_ref, audit_start,
ignore_missing_network_data=True):
"""Get bandwidth usage information for the instance for the
specified audit period.
"""
admin_context = nova.context.get_admin_context(read_deleted='yes')
if (instance_ref.get('info_cache') and
instance_ref['info_cache'].get('network_info') is not None):
cached_info = instance_ref['info_cache']['network_info']
nw_info = network_model.NetworkInfo.hydrate(cached_info)
else:
try:
nw_info = network.API().get_instance_nw_info(admin_context,
instance_ref)
except Exception:
try:
with excutils.save_and_reraise_exception():
LOG.exception(_('Failed to get nw_info'),
instance=instance_ref)
except Exception:
if ignore_missing_network_data:
return
raise
macs = [vif['address'] for vif in nw_info]
uuids = [instance_ref["uuid"]]
bw_usages = db.bw_usage_get_by_uuids(admin_context, uuids, audit_start)
bw_usages = [b for b in bw_usages if b.mac in macs]
bw = {}
for b in bw_usages:
label = 'net-name-not-found-%s' % b['mac']
for vif in nw_info:
if vif['address'] == b['mac']:
label = vif['network']['label']
break
bw[label] = dict(bw_in=b.bw_in, bw_out=b.bw_out)
return bw
def image_meta(system_metadata):
"""Format image metadata for use in notifications from the instance
system metadata.
"""
image_meta = {}
for md_key, md_value in system_metadata.iteritems():
if md_key.startswith('image_'):
image_meta[md_key[6:]] = md_value
return image_meta
def info_from_instance(context, instance_ref, network_info,
system_metadata, **kw):
"""Get detailed instance information for an instance which is common to all
notifications.
:param network_info: network_info provided if not None
:param system_metadata: system_metadata DB entries for the instance,
if not None. *NOTE*: Currently unused here in trunk, but needed for
potential custom modifications.
"""
def null_safe_str(s):
return str(s) if s else ''
image_ref_url = glance.generate_image_url(instance_ref['image_ref'])
instance_type = instance_types.extract_instance_type(instance_ref)
instance_type_name = instance_type.get('name', '')
if system_metadata is None:
system_metadata = utils.metadata_to_dict(
instance_ref['system_metadata'])
instance_info = dict(
# Owner properties
tenant_id=instance_ref['project_id'],
user_id=instance_ref['user_id'],
# Identity properties
instance_id=instance_ref['uuid'],
display_name=instance_ref['display_name'],
reservation_id=instance_ref['reservation_id'],
hostname=instance_ref['hostname'],
# Type properties
instance_type=instance_type_name,
instance_type_id=instance_ref['instance_type_id'],
architecture=instance_ref['architecture'],
# Capacity properties
memory_mb=instance_ref['memory_mb'],
disk_gb=instance_ref['root_gb'] + instance_ref['ephemeral_gb'],
vcpus=instance_ref['vcpus'],
# Note(dhellmann): This makes the disk_gb value redundant, but
# we are keeping it for backwards-compatibility with existing
# users of notifications.
root_gb=instance_ref['root_gb'],
ephemeral_gb=instance_ref['ephemeral_gb'],
# Location properties
host=instance_ref['host'],
availability_zone=instance_ref['availability_zone'],
# Date properties
created_at=str(instance_ref['created_at']),
# Nova's deleted vs terminated instance terminology is confusing,
# this should be when the instance was deleted (i.e. terminated_at),
# not when the db record was deleted. (mdragon)
deleted_at=null_safe_str(instance_ref.get('terminated_at')),
launched_at=null_safe_str(instance_ref.get('launched_at')),
# Image properties
image_ref_url=image_ref_url,
os_type=instance_ref['os_type'],
kernel_id=instance_ref['kernel_id'],
ramdisk_id=instance_ref['ramdisk_id'],
# Status properties
state=instance_ref['vm_state'],
state_description=null_safe_str(instance_ref.get('task_state')),
# accessIPs
access_ip_v4=instance_ref['access_ip_v4'],
access_ip_v6=instance_ref['access_ip_v6'],
)
if network_info is not None:
fixed_ips = []
for vif in network_info:
for ip in vif.fixed_ips():
ip["label"] = vif["network"]["label"]
fixed_ips.append(ip)
instance_info['fixed_ips'] = fixed_ips
# add image metadata
image_meta_props = image_meta(system_metadata)
instance_info["image_meta"] = image_meta_props
# add instance metadata
instance_info['metadata'] = instance_ref['metadata']
instance_info.update(kw)
return instance_info