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

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. 

16 

17"""Functionality related to notifications common to multiple layers of 

18the system. 

19""" 

20 

21import datetime 

22 

23from keystoneauth1 import exceptions as ks_exc 

24from oslo_log import log 

25from oslo_utils import excutils 

26from oslo_utils import timeutils 

27 

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 

37 

38 

39LOG = log.getLogger(__name__) 

40 

41CONF = nova.conf.CONF 

42 

43 

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 """ 

49 

50 if not CONF.notifications.notify_on_state_change: 

51 # skip all this if updates are disabled 

52 return 

53 

54 update_with_state_change = False 

55 

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"] 

60 

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 

70 

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) 

77 

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) 

93 

94 

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 """ 

101 

102 if not CONF.notifications.notify_on_state_change: 

103 # skip all this if updates are disabled 

104 return 

105 

106 fire_update = True 

107 # send update notification by default 

108 

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 

121 

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) 

136 

137 

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"] 

156 

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 

164 

165 

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) 

180 

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)) 

186 

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) 

191 

192 # add old display name if it is changed 

193 if old_display_name: 

194 payload["old_display_name"] = old_display_name 

195 

196 rpc.get_notifier(service, host).info(context, 

197 'compute.instance.update', payload) 

198 

199 _send_versioned_instance_update(context, instance, payload, host, service) 

200 

201 

202@rpc.if_notifications_enabled 

203def _send_versioned_instance_update(context, instance, payload, host, service): 

204 

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 

210 

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')) 

216 

217 audit_period = instance_notification.AuditPeriodPayload( 

218 audit_period_beginning=payload.get('audit_period_beginning'), 

219 audit_period_ending=payload.get('audit_period_ending')) 

220 

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')) 

227 

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) 

238 

239 

240def audit_period_bounds(current_period=False): 

241 """Get the start and end of the relevant audit usage period 

242 

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 """ 

247 

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 

255 

256 return (audit_start, audit_end) 

257 

258 

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 

267 

268 return image_meta 

269 

270 

271def null_safe_str(s): 

272 return str(s) if s else '' 

273 

274 

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 '' 

280 

281 

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. 

286 

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) 

304 

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 

315 

316 flavor = instance.get_flavor() 

317 flavor_name = flavor.get('name', '') 

318 instance_flavorid = flavor.get('flavorid', '') 

319 

320 instance_info = dict( 

321 # Owner properties 

322 tenant_id=instance.project_id, 

323 user_id=instance.user_id, 

324 

325 # Identity properties 

326 instance_id=instance.uuid, 

327 display_name=instance.display_name, 

328 reservation_id=instance.reservation_id, 

329 hostname=instance.hostname, 

330 

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, 

336 

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, 

346 

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), 

352 

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)), 

361 

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, 

367 

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 '', 

376 

377 # accessIPs 

378 access_ip_v4=instance.access_ip_v4, 

379 access_ip_v6=instance.access_ip_v6, 

380 ) 

381 

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 

390 

391 # add image metadata 

392 image_meta_props = image_meta(instance.system_metadata) 

393 instance_info["image_meta"] = image_meta_props 

394 

395 # add instance metadata 

396 instance_info['metadata'] = instance.metadata 

397 

398 instance_info.update(kw) 

399 return instance_info