Coverage for nova/notifications/base.py: 99%
136 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-17 15:08 +0000
1# Copyright (c) 2012 OpenStack Foundation
2# All Rights Reserved.
3# Copyright 2013 Red Hat, Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may
6# not use this file except in compliance with the License. You may obtain
7# a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations
15# under the License.
17"""Functionality related to notifications common to multiple layers of
18the system.
19"""
21import datetime
23from keystoneauth1 import exceptions as ks_exc
24from oslo_log import log
25from oslo_utils import excutils
26from oslo_utils import timeutils
28import nova.conf
29import nova.context
30from nova import exception
31from nova.image import glance
32from nova.notifications.objects import base as notification_base
33from nova.notifications.objects import instance as instance_notification
34from nova.objects import fields
35from nova import rpc
36from nova import utils
39LOG = log.getLogger(__name__)
41CONF = nova.conf.CONF
44def send_update(context, old_instance, new_instance, service="compute",
45 host=None):
46 """Send compute.instance.update notification to report any changes occurred
47 in that instance
48 """
50 if not CONF.notifications.notify_on_state_change:
51 # skip all this if updates are disabled
52 return
54 update_with_state_change = False
56 old_vm_state = old_instance["vm_state"]
57 new_vm_state = new_instance["vm_state"]
58 old_task_state = old_instance["task_state"]
59 new_task_state = new_instance["task_state"]
61 # we should check if we need to send a state change or a regular
62 # notification
63 if old_vm_state != new_vm_state:
64 # yes, the vm state is changing:
65 update_with_state_change = True
66 elif (CONF.notifications.notify_on_state_change == "vm_and_task_state" and
67 old_task_state != new_task_state):
68 # yes, the task state is changing:
69 update_with_state_change = True
71 if update_with_state_change:
72 # send a notification with state changes
73 # value of verify_states need not be True as the check for states is
74 # already done here
75 send_update_with_states(context, new_instance, old_vm_state,
76 new_vm_state, old_task_state, new_task_state, service, host)
78 else:
79 try:
80 old_display_name = None
81 if new_instance["display_name"] != old_instance["display_name"]:
82 old_display_name = old_instance["display_name"]
83 send_instance_update_notification(context, new_instance,
84 service=service, host=host,
85 old_display_name=old_display_name)
86 except exception.InstanceNotFound:
87 LOG.debug('Failed to send instance update notification. The '
88 'instance could not be found and was most likely '
89 'deleted.', instance=new_instance)
90 except Exception:
91 LOG.exception("Failed to send state update notification",
92 instance=new_instance)
95def send_update_with_states(context, instance, old_vm_state, new_vm_state,
96 old_task_state, new_task_state, service="compute", host=None,
97 verify_states=False):
98 """Send compute.instance.update notification to report changes if there
99 are any, in the instance
100 """
102 if not CONF.notifications.notify_on_state_change:
103 # skip all this if updates are disabled
104 return
106 fire_update = True
107 # send update notification by default
109 if verify_states:
110 # check whether we need to send notification related to state changes
111 fire_update = False
112 # do not send notification if the conditions for vm and(or) task state
113 # are not satisfied
114 if old_vm_state != new_vm_state:
115 # yes, the vm state is changing:
116 fire_update = True
117 elif (CONF.notifications.notify_on_state_change ==
118 "vm_and_task_state" and old_task_state != new_task_state):
119 # yes, the task state is changing:
120 fire_update = True
122 if fire_update:
123 # send either a state change or a regular notification
124 try:
125 send_instance_update_notification(context, instance,
126 old_vm_state=old_vm_state, old_task_state=old_task_state,
127 new_vm_state=new_vm_state, new_task_state=new_task_state,
128 service=service, host=host)
129 except exception.InstanceNotFound:
130 LOG.debug('Failed to send instance update notification. The '
131 'instance could not be found and was most likely '
132 'deleted.', instance=instance)
133 except Exception:
134 LOG.exception("Failed to send state update notification",
135 instance=instance)
138def _compute_states_payload(instance, old_vm_state=None,
139 old_task_state=None, new_vm_state=None, new_task_state=None):
140 # If the states were not specified we assume the current instance
141 # states are the correct information. This is important to do for
142 # both old and new states because otherwise we create some really
143 # confusing notifications like:
144 #
145 # None(None) => Building(none)
146 #
147 # When we really were just continuing to build
148 if new_vm_state is None:
149 new_vm_state = instance["vm_state"]
150 if new_task_state is None:
151 new_task_state = instance["task_state"]
152 if old_vm_state is None:
153 old_vm_state = instance["vm_state"]
154 if old_task_state is None:
155 old_task_state = instance["task_state"]
157 states_payload = {
158 "old_state": old_vm_state,
159 "state": new_vm_state,
160 "old_task_state": old_task_state,
161 "new_task_state": new_task_state,
162 }
163 return states_payload
166def send_instance_update_notification(context, instance, old_vm_state=None,
167 old_task_state=None, new_vm_state=None, new_task_state=None,
168 service="compute", host=None, old_display_name=None):
169 """Send 'compute.instance.update' notification to inform observers
170 about instance state changes.
171 """
172 # NOTE(gibi): The image_ref_url is only used in unversioned notifications.
173 # Calling the generate_image_url() could be costly as it calls
174 # the Keystone API. So only do the call if the actual value will be
175 # used.
176 populate_image_ref_url = (CONF.notifications.notification_format in
177 ('both', 'unversioned'))
178 payload = info_from_instance(context, instance, None,
179 populate_image_ref_url=populate_image_ref_url)
181 # determine how we'll report states
182 payload.update(
183 _compute_states_payload(
184 instance, old_vm_state, old_task_state,
185 new_vm_state, new_task_state))
187 # add audit fields:
188 (audit_start, audit_end) = audit_period_bounds(current_period=True)
189 payload["audit_period_beginning"] = null_safe_isotime(audit_start)
190 payload["audit_period_ending"] = null_safe_isotime(audit_end)
192 # add old display name if it is changed
193 if old_display_name:
194 payload["old_display_name"] = old_display_name
196 rpc.get_notifier(service, host).info(context,
197 'compute.instance.update', payload)
199 _send_versioned_instance_update(context, instance, payload, host, service)
202@rpc.if_notifications_enabled
203def _send_versioned_instance_update(context, instance, payload, host, service):
205 def _map_legacy_service_to_source(legacy_service):
206 if not legacy_service.startswith('nova-'):
207 return 'nova-' + service
208 else:
209 return service
211 state_update = instance_notification.InstanceStateUpdatePayload(
212 old_state=payload.get('old_state'),
213 state=payload.get('state'),
214 old_task_state=payload.get('old_task_state'),
215 new_task_state=payload.get('new_task_state'))
217 audit_period = instance_notification.AuditPeriodPayload(
218 audit_period_beginning=payload.get('audit_period_beginning'),
219 audit_period_ending=payload.get('audit_period_ending'))
221 versioned_payload = instance_notification.InstanceUpdatePayload(
222 context=context,
223 instance=instance,
224 state_update=state_update,
225 audit_period=audit_period,
226 old_display_name=payload.get('old_display_name'))
228 notification = instance_notification.InstanceUpdateNotification(
229 priority=fields.NotificationPriority.INFO,
230 event_type=notification_base.EventType(
231 object='instance',
232 action=fields.NotificationAction.UPDATE),
233 publisher=notification_base.NotificationPublisher(
234 host=host or CONF.host,
235 source=_map_legacy_service_to_source(service)),
236 payload=versioned_payload)
237 notification.emit(context)
240def audit_period_bounds(current_period=False):
241 """Get the start and end of the relevant audit usage period
243 :param current_period: if True, this will generate a usage for the
244 current usage period; if False, this will generate a usage for the
245 previous audit period.
246 """
248 begin, end = utils.last_completed_audit_period()
249 if current_period:
250 audit_start = end
251 audit_end = timeutils.utcnow()
252 else:
253 audit_start = begin
254 audit_end = end
256 return (audit_start, audit_end)
259def image_meta(system_metadata):
260 """Format image metadata for use in notifications from the instance
261 system metadata.
262 """
263 image_meta = {}
264 for md_key, md_value in system_metadata.items():
265 if md_key.startswith('image_'):
266 image_meta[md_key[6:]] = md_value
268 return image_meta
271def null_safe_str(s):
272 return str(s) if s else ''
275def null_safe_isotime(s):
276 if isinstance(s, datetime.datetime):
277 return utils.strtime(s)
278 else:
279 return str(s) if s else ''
282def info_from_instance(context, instance, network_info,
283 populate_image_ref_url=False, **kw):
284 """Get detailed instance information for an instance which is common to all
285 notifications.
287 :param:instance: nova.objects.Instance
288 :param:network_info: network_info provided if not None
289 :param:populate_image_ref_url: If True then the full URL of the image of
290 the instance is generated and returned.
291 This, depending on the configuration, might
292 mean a call to Keystone. If false, None
293 value is returned in the dict at the
294 image_ref_url key.
295 """
296 image_ref_url = None
297 if populate_image_ref_url:
298 try:
299 # NOTE(mriedem): We can eventually drop this when we no longer
300 # support legacy notifications since versioned notifications don't
301 # use this.
302 image_ref_url = glance.API().generate_image_url(
303 instance.image_ref, context)
305 except ks_exc.EndpointNotFound:
306 # We might be running from a periodic task with no auth token and
307 # CONF.glance.api_servers isn't set, so we can't get the image API
308 # endpoint URL from the service catalog, therefore just use the
309 # image id for the URL (yes it's a lie, but it's best effort at
310 # this point).
311 with excutils.save_and_reraise_exception() as exc_ctx:
312 if context.auth_token is None:
313 image_ref_url = instance.image_ref
314 exc_ctx.reraise = False
316 flavor = instance.get_flavor()
317 flavor_name = flavor.get('name', '')
318 instance_flavorid = flavor.get('flavorid', '')
320 instance_info = dict(
321 # Owner properties
322 tenant_id=instance.project_id,
323 user_id=instance.user_id,
325 # Identity properties
326 instance_id=instance.uuid,
327 display_name=instance.display_name,
328 reservation_id=instance.reservation_id,
329 hostname=instance.hostname,
331 # Type properties
332 instance_type=flavor_name,
333 instance_type_id=instance.instance_type_id,
334 instance_flavor_id=instance_flavorid,
335 architecture=instance.architecture,
337 # Capacity properties
338 memory_mb=instance.flavor.memory_mb,
339 disk_gb=instance.flavor.root_gb + instance.flavor.ephemeral_gb,
340 vcpus=instance.flavor.vcpus,
341 # Note(dhellmann): This makes the disk_gb value redundant, but
342 # we are keeping it for backwards-compatibility with existing
343 # users of notifications.
344 root_gb=instance.flavor.root_gb,
345 ephemeral_gb=instance.flavor.ephemeral_gb,
347 # Location properties
348 host=instance.host,
349 node=instance.node,
350 availability_zone=instance.availability_zone,
351 cell_name=null_safe_str(instance.cell_name),
353 # Date properties
354 created_at=str(instance.created_at),
355 # Terminated and Deleted are slightly different (although being
356 # terminated and not deleted is a transient state), so include
357 # both and let the recipient decide which they want to use.
358 terminated_at=null_safe_isotime(instance.get('terminated_at', None)),
359 deleted_at=null_safe_isotime(instance.get('deleted_at', None)),
360 launched_at=null_safe_isotime(instance.get('launched_at', None)),
362 # Image properties
363 image_ref_url=image_ref_url,
364 os_type=instance.os_type,
365 kernel_id=instance.kernel_id,
366 ramdisk_id=instance.ramdisk_id,
368 # Status properties
369 state=instance.vm_state,
370 state_description=null_safe_str(instance.task_state),
371 # NOTE(gibi): It might seems wrong to default the progress to an empty
372 # string but this is how legacy work and this code only used by the
373 # legacy notification so try to keep the compatibility here but also
374 # keep it contained.
375 progress=int(instance.progress) if instance.progress else '',
377 # accessIPs
378 access_ip_v4=instance.access_ip_v4,
379 access_ip_v6=instance.access_ip_v6,
380 )
382 if network_info is not None:
383 fixed_ips = []
384 for vif in network_info:
385 for ip in vif.fixed_ips():
386 ip["label"] = vif["network"]["label"]
387 ip["vif_mac"] = vif["address"]
388 fixed_ips.append(ip)
389 instance_info['fixed_ips'] = fixed_ips
391 # add image metadata
392 image_meta_props = image_meta(instance.system_metadata)
393 instance_info["image_meta"] = image_meta_props
395 # add instance metadata
396 instance_info['metadata'] = instance.metadata
398 instance_info.update(kw)
399 return instance_info