blob: cf1ccfa76e91ebc1ba69477c6ff2a5be0aff0e56 [file] [log] [blame]
okozachenko179cd572021-05-06 22:23:53 +03001import parse
okozachenko6d277e32021-05-05 20:23:32 +03002import staffeln.conf
3import collections
4from staffeln.common import constants
okozachenkocb316082021-05-07 21:30:39 +03005from staffeln.conductor import result
okozachenkod2801362021-05-05 21:23:46 +03006from openstack.exceptions import ResourceNotFound as OpenstackResourceNotFound
7from openstack.exceptions import SDKException as OpenstackSDKException
okozachenko6d277e32021-05-05 20:23:32 +03008from oslo_log import log
okozachenko6d277e32021-05-05 20:23:32 +03009from staffeln.common import context
10from staffeln import objects
11from staffeln.i18n import _
okozachenkob7d55372021-05-10 18:21:02 +030012from staffeln.common import openstack
okozachenko6d277e32021-05-05 20:23:32 +030013
14CONF = staffeln.conf.CONF
15LOG = log.getLogger(__name__)
16
17BackupMapping = collections.namedtuple(
okozachenko179cd572021-05-06 22:23:53 +030018 "BackupMapping", ["volume_id", "backup_id", "project_id", "instance_id", "backup_completed"]
okozachenko6d277e32021-05-05 20:23:32 +030019)
20
21QueueMapping = collections.namedtuple(
okozachenko179cd572021-05-06 22:23:53 +030022 "QueueMapping", ["volume_id", "backup_id", "project_id", "instance_id", "backup_status"]
okozachenko6d277e32021-05-05 20:23:32 +030023)
24
okozachenko6d277e32021-05-05 20:23:32 +030025
26def check_vm_backup_metadata(metadata):
27 if not CONF.conductor.backup_metadata_key in metadata:
28 return False
29 return metadata[CONF.conductor.backup_metadata_key].lower() in ["true"]
30
31
okozachenko6d277e32021-05-05 20:23:32 +030032class Backup(object):
33 """Implmentations of the queue with the sql."""
34
35 def __init__(self):
36 self.ctx = context.make_context()
okozachenkocb316082021-05-07 21:30:39 +030037 self.result = result.BackupResult()
okozachenkob7d55372021-05-10 18:21:02 +030038 self.openstacksdk = openstack.OpenstackSDK()
okozachenkof731e622021-05-10 17:38:30 +030039 self.project_list = {}
okozachenkocb316082021-05-07 21:30:39 +030040
41 def publish_backup_result(self):
42 self.result.publish()
okozachenko6d277e32021-05-05 20:23:32 +030043
okozachenko9eae01d2021-05-11 16:47:27 +030044 def refresh_backup_result(self):
45 self.result.initialize()
46
okozachenko6d277e32021-05-05 20:23:32 +030047 def get_backups(self, filters=None):
48 return objects.Volume.list(self.ctx, filters=filters)
49
okozachenkofb385602021-05-07 19:00:47 +030050 def get_backup_quota(self, project_id):
okozachenkof731e622021-05-10 17:38:30 +030051 return self.openstacksdk.get_backup_quota(project_id)
okozachenkofb385602021-05-07 19:00:47 +030052
okozachenko6d277e32021-05-05 20:23:32 +030053 def get_queues(self, filters=None):
54 """Get the list of volume queue columns from the queue_data table"""
55 queues = objects.Queue.list(self.ctx, filters=filters)
56 return queues
57
okozachenkod2801362021-05-05 21:23:46 +030058 def create_queue(self, old_tasks):
okozachenko6d277e32021-05-05 20:23:32 +030059 """Create the queue of all the volumes for backup"""
okozachenkod2801362021-05-05 21:23:46 +030060 # 1. get the old task list, not finished in the last cycle
61 # and keep till now
62 old_task_volume_list = []
63 for old_task in old_tasks:
64 old_task_volume_list.append(old_task.volume_id)
65
66 # 2. add new tasks in the queue which are not existing in the old task list
okozachenko6d277e32021-05-05 20:23:32 +030067 queue_list = self.check_instance_volumes()
68 for queue in queue_list:
okozachenkod2801362021-05-05 21:23:46 +030069 if not queue.volume_id in old_task_volume_list:
70 self._volume_queue(queue)
okozachenko6d277e32021-05-05 20:23:32 +030071
72 # Backup the volumes attached to which has a specific metadata
okozachenkofb385602021-05-07 19:00:47 +030073 def filter_by_server_metadata(self, metadata):
okozachenko6d277e32021-05-05 20:23:32 +030074
75 if not CONF.conductor.backup_metadata_key in metadata:
76 return False
77
78 return metadata[CONF.conductor.backup_metadata_key].lower() == constants.BACKUP_ENABLED_KEY
79
80 # Backup the volumes in in-use and available status
okozachenkocb316082021-05-07 21:30:39 +030081 def filter_by_volume_status(self, volume_id, project_id):
okozachenko6d277e32021-05-05 20:23:32 +030082 try:
okozachenkof731e622021-05-10 17:38:30 +030083 volume = self.openstacksdk.get_volume(volume_id, project_id)
okozachenko6d277e32021-05-05 20:23:32 +030084 if volume == None: return False
85 res = volume['status'] in ("available", "in-use")
86 if not res:
okozachenkocb316082021-05-07 21:30:39 +030087 reason = _("Volume %s is not backed because it is in %s status" % (volume_id, volume['status']))
88 LOG.info(reason)
89 self.result.add_failed_backup(project_id, volume_id, reason)
okozachenko6d277e32021-05-05 20:23:32 +030090 return res
91
okozachenkod2801362021-05-05 21:23:46 +030092 except OpenstackResourceNotFound:
okozachenko6d277e32021-05-05 20:23:32 +030093 return False
94
okozachenkoca2b37a2021-05-06 20:38:02 +030095 # delete all backups forcily regardless of the status
okozachenko2ab48e82021-05-07 16:55:11 +030096 def hard_cancel_backup_task(self, task):
okozachenkoca2b37a2021-05-06 20:38:02 +030097 try:
okozachenkof731e622021-05-10 17:38:30 +030098 project_id = task.project_id
okozachenkocb316082021-05-07 21:30:39 +030099 reason = _("Cancel backup %s because of timeout." % task.backup_id)
100 LOG.info(reason)
okozachenkof731e622021-05-10 17:38:30 +0300101
102 if project_id not in self.project_list: self.process_non_existing_backup(task)
103 self.openstacksdk.set_project(self.project_list[project_id])
104 backup = self.openstacksdk.get_backup(task.backup_id)
okozachenkoca2b37a2021-05-06 20:38:02 +0300105 if backup == None: return task.delete_queue()
okozachenkof731e622021-05-10 17:38:30 +0300106 self.openstacksdk.delete_backup(task.backup_id)
okozachenkoca2b37a2021-05-06 20:38:02 +0300107 task.delete_queue()
okozachenkocb316082021-05-07 21:30:39 +0300108 self.result.add_failed_backup(task.project_id, task.volume_id, reason)
okozachenkoca2b37a2021-05-06 20:38:02 +0300109 except OpenstackResourceNotFound:
110 task.delete_queue()
111
112 except OpenstackSDKException as e:
okozachenkocb316082021-05-07 21:30:39 +0300113 reason = _("Backup %s deletion failed."
114 "%s" % (task.backup_id, str(e)))
115 LOG.info(reason)
116 # TODO(Alex): If backup timeout and also back cancel failed,
117 # then what to do?
okozachenkoca2b37a2021-05-06 20:38:02 +0300118 # 1. notify
119 # 2. set the volume status as in-use
120 # remove from the queue table
121 task.delete_queue()
okozachenkocb316082021-05-07 21:30:39 +0300122 self.result.add_failed_backup(task.project_id, task.volume_id, reason)
okozachenkoca2b37a2021-05-06 20:38:02 +0300123
okozachenkof731e622021-05-10 17:38:30 +0300124 # delete only available backups: reserved
okozachenko2ab48e82021-05-07 16:55:11 +0300125 def soft_remove_backup_task(self, backup_object):
okozachenko6d277e32021-05-05 20:23:32 +0300126 try:
okozachenkof731e622021-05-10 17:38:30 +0300127 backup = self.openstacksdk.get_backup(backup_object.backup_id)
okozachenkoca2b37a2021-05-06 20:38:02 +0300128 if backup == None: return backup_object.delete_backup()
okozachenko6d277e32021-05-05 20:23:32 +0300129 if backup["status"] in ("available"):
okozachenkof731e622021-05-10 17:38:30 +0300130 self.openstacksdk.delete_backup(backup_object.backup_id)
okozachenko6d277e32021-05-05 20:23:32 +0300131 backup_object.delete_backup()
132 elif backup["status"] in ("error", "error_restoring"):
133 # TODO(Alex): need to discuss
134 # now if backup is in error status, then retention service
135 # does not remove it from openstack but removes it from the
136 # backup table so user can delete it on Horizon.
137 backup_object.delete_backup()
138 else: # "deleting", "restoring"
139 LOG.info(_("Rotation for the backup %s is skipped in this cycle "
140 "because it is in %s status") % (backup_object.backup_id, backup["status"]))
141
okozachenkod2801362021-05-05 21:23:46 +0300142 except OpenstackResourceNotFound:
okozachenko6d277e32021-05-05 20:23:32 +0300143 LOG.info(_("Backup %s is not existing in Openstack."
144 "Or cinder-backup is not existing in the cloud." % backup_object.backup_id))
145 # remove from the backup table
146 backup_object.delete_backup()
147 return False
148
okozachenkoca2b37a2021-05-06 20:38:02 +0300149 except OpenstackSDKException as e:
okozachenkoa6dcdc02021-05-10 14:13:26 +0300150 LOG.info(
151 _("Backup %s deletion failed." "%s" % (backup_object.backup_id,
152 str(e)))
153 )
okozachenkoca2b37a2021-05-06 20:38:02 +0300154 # TODO(Alex): Add it into the notification queue
155 # remove from the backup table
156 backup_object.delete_backup()
157 return False
158
okozachenkoca2b37a2021-05-06 20:38:02 +0300159 # delete all backups forcily regardless of the status
160 def hard_remove_volume_backup(self, backup_object):
161 try:
okozachenkob875cbe2021-05-11 19:29:51 +0300162 project_id = backup_object.project_id
163 if project_id not in self.project_list:
164 backup_object.delete_backup()
165
166 self.openstacksdk.set_project(project_id)
okozachenkof731e622021-05-10 17:38:30 +0300167 backup = self.openstacksdk.get_backup(uuid=backup_object.backup_id,
okozachenkob875cbe2021-05-11 19:29:51 +0300168 project_id=project_id)
okozachenkoca2b37a2021-05-06 20:38:02 +0300169 if backup == None: return backup_object.delete_backup()
170
okozachenkof731e622021-05-10 17:38:30 +0300171 self.openstacksdk.delete_backup(uuid=backup_object.backup_id)
okozachenkoca2b37a2021-05-06 20:38:02 +0300172 backup_object.delete_backup()
173
174 except OpenstackResourceNotFound:
175 LOG.info(_("Backup %s is not existing in Openstack."
176 "Or cinder-backup is not existing in the cloud." % backup_object.backup_id))
177 # remove from the backup table
178 backup_object.delete_backup()
179
180 except OpenstackSDKException as e:
okozachenkoa6dcdc02021-05-10 14:13:26 +0300181 LOG.info(
182 _("Backup %s deletion failed." "%s" % (backup_object.backup_id,
183 str(e)))
184 )
185
okozachenkoca2b37a2021-05-06 20:38:02 +0300186 # TODO(Alex): Add it into the notification queue
187 # remove from the backup table
188 backup_object.delete_backup()
189
okozachenkob875cbe2021-05-11 19:29:51 +0300190 def update_project_list(self):
191 projects = self.openstacksdk.get_projects()
192 for project in projects:
193 self.project_list[project.id] = project
194
okozachenko6d277e32021-05-05 20:23:32 +0300195 def check_instance_volumes(self):
196 """Get the list of all the volumes from the project using openstacksdk
197 Function first list all the servers in the project and get the volumes
198 that are attached to the instance.
199 """
200 queues_map = []
okozachenkof731e622021-05-10 17:38:30 +0300201 projects = self.openstacksdk.get_projects()
okozachenko6d277e32021-05-05 20:23:32 +0300202 for project in projects:
okozachenkocb316082021-05-07 21:30:39 +0300203 empty_project = True
okozachenkof731e622021-05-10 17:38:30 +0300204 self.project_list[project.id] = project
205 servers = self.openstacksdk.get_servers(project_id=project.id)
okozachenko6d277e32021-05-05 20:23:32 +0300206 for server in servers:
okozachenkofb385602021-05-07 19:00:47 +0300207 if not self.filter_by_server_metadata(server.metadata): continue
okozachenkocb316082021-05-07 21:30:39 +0300208 if empty_project:
209 empty_project = False
210 self.result.add_project(project.id, project.name)
okozachenko6d277e32021-05-05 20:23:32 +0300211 for volume in server.attached_volumes:
okozachenkocb316082021-05-07 21:30:39 +0300212 if not self.filter_by_volume_status(volume["id"], project.id): continue
okozachenko6d277e32021-05-05 20:23:32 +0300213 queues_map.append(
214 QueueMapping(
okozachenko179cd572021-05-06 22:23:53 +0300215 project_id=project.id,
okozachenko6d277e32021-05-05 20:23:32 +0300216 volume_id=volume["id"],
217 backup_id="NULL",
218 instance_id=server.id,
219 backup_status=constants.BACKUP_PLANNED,
220 )
221 )
222 return queues_map
223
224 def _volume_queue(self, task):
225 """Saves the queue data to the database."""
226
227 # TODO(Alex): Need to escalate discussion
228 # When create the task list, need to check the WIP backup generators
229 # which are created in the past backup cycle.
230 # Then skip to create new tasks for the volumes whose backup is WIP
231 volume_queue = objects.Queue(self.ctx)
232 volume_queue.backup_id = task.backup_id
233 volume_queue.volume_id = task.volume_id
234 volume_queue.instance_id = task.instance_id
Susanta Gautamc4061112021-05-10 02:39:23 +0000235 volume_queue.project_id = task.project_id
okozachenko6d277e32021-05-05 20:23:32 +0300236 volume_queue.backup_status = task.backup_status
237 volume_queue.create()
238
239 def create_volume_backup(self, queue):
240 """Initiate the backup of the volume
241 :params: queue: Provide the map of the volume that needs
242 backup.
243 This function will call the backupup api and change the
244 backup_status and backup_id in the queue table.
245 """
okozachenkof731e622021-05-10 17:38:30 +0300246 project_id = queue.project_id
247 if queue.backup_id == "NULL":
okozachenko6d277e32021-05-05 20:23:32 +0300248 try:
okozachenkocb316082021-05-07 21:30:39 +0300249 LOG.info(_("Backup for volume %s creating in project %s"
okozachenkof731e622021-05-10 17:38:30 +0300250 % (queue.volume_id, project_id)))
okozachenko179cd572021-05-06 22:23:53 +0300251 # NOTE(Alex): no need to wait because we have a cycle time out
okozachenkof731e622021-05-10 17:38:30 +0300252 if project_id not in self.project_list: self.process_non_existing_backup(queue)
253 self.openstacksdk.set_project(self.project_list[project_id])
254 volume_backup = self.openstacksdk.create_backup(volume_id=queue.volume_id,
okozachenkob875cbe2021-05-11 19:29:51 +0300255 project_id=project_id)
okozachenko179cd572021-05-06 22:23:53 +0300256 queue.backup_id = volume_backup.id
257 queue.backup_status = constants.BACKUP_WIP
258 queue.save()
okozachenkod2801362021-05-05 21:23:46 +0300259 except OpenstackSDKException as error:
okozachenkocb316082021-05-07 21:30:39 +0300260 reason = _("Backup creation for the volume %s failled. %s"
261 % (queue.volume_id, str(error)))
262 LOG.info(reason)
okozachenkof731e622021-05-10 17:38:30 +0300263 self.result.add_failed_backup(project_id, queue.volume_id, reason)
okozachenko179cd572021-05-06 22:23:53 +0300264 parsed = parse.parse("Error in creating volume backup {id}", str(error))
okozachenkoa6dcdc02021-05-10 14:13:26 +0300265 if parsed is not None:
266 queue.backup_id = parsed["id"]
okozachenko179cd572021-05-06 22:23:53 +0300267 queue.backup_status = constants.BACKUP_WIP
268 queue.save()
okozachenko6d277e32021-05-05 20:23:32 +0300269 else:
270 pass
271 # TODO(Alex): remove this task from the task list
272 # Backup planned task cannot have backup_id in the same cycle
273 # Reserve for now because it is related to the WIP backup genenrators which
274 # are not finished in the current cycle
275
okozachenkoa6dcdc02021-05-10 14:13:26 +0300276 # backup gen was not created
277 def process_pre_failed_backup(self, task):
278 # 1.notify via email
279 reason = _("The backup creation for the volume %s was prefailed."
280 % task.volume_id)
281 self.result.add_failed_backup(task.project_id, task.volume_id, reason)
282 # LOG.error(reason)
283 # 2. remove failed task from the task queue
284 task.delete_queue()
285
okozachenko6d277e32021-05-05 20:23:32 +0300286 def process_failed_backup(self, task):
okozachenkof731e622021-05-10 17:38:30 +0300287 # 1. notify via email
okozachenkocb316082021-05-07 21:30:39 +0300288 reason = _("The status of backup for the volume %s is error." % task.volume_id)
289 self.result.add_failed_backup(task.project_id, task.volume_id, reason)
290 LOG.error(reason)
okozachenkof731e622021-05-10 17:38:30 +0300291 # 2. delete backup generator
292 self.openstacksdk.delete_backup(uuid=task.backup_id)
okozachenko6d277e32021-05-05 20:23:32 +0300293 # 3. remove failed task from the task queue
294 task.delete_queue()
295
okozachenko179cd572021-05-06 22:23:53 +0300296 def process_non_existing_backup(self, task):
297 task.delete_queue()
298
okozachenko6d277e32021-05-05 20:23:32 +0300299 def process_available_backup(self, task):
300 LOG.info("Backup of the volume %s is successful." % task.volume_id)
301 # 1. save success backup in the backup table
302 self._volume_backup(
303 BackupMapping(
304 volume_id=task.volume_id,
okozachenko179cd572021-05-06 22:23:53 +0300305 project_id=task.project_id,
okozachenko6d277e32021-05-05 20:23:32 +0300306 backup_id=task.backup_id,
307 instance_id=task.instance_id,
308 backup_completed=1,
309 )
310 )
okozachenkocb316082021-05-07 21:30:39 +0300311 self.result.add_success_backup(task.project_id, task.volume_id, task.backup_id)
okozachenko6d277e32021-05-05 20:23:32 +0300312 # 2. remove from the task list
313 task.delete_queue()
314 # 3. TODO(Alex): notify via email
315
316 def process_using_backup(self, task):
okozachenkocb316082021-05-07 21:30:39 +0300317 # treat same as the available backup for now
318 self.process_available_backup(task)
okozachenko6d277e32021-05-05 20:23:32 +0300319
320 def check_volume_backup_status(self, queue):
321 """Checks the backup status of the volume
322 :params: queue: Provide the map of the volume that needs backup
323 status checked.
324 Call the backups api to see if the backup is successful.
325 """
okozachenkod4284d22021-05-05 20:48:23 +0300326 try:
okozachenkoed4c4872021-05-10 17:58:37 +0300327
okozachenkof731e622021-05-10 17:38:30 +0300328 project_id = queue.project_id
okozachenkoed4c4872021-05-10 17:58:37 +0300329
okozachenkoa6dcdc02021-05-10 14:13:26 +0300330 # The case in which the error produced before backup gen created.
331 if queue.backup_id == "NULL":
332 self.process_pre_failed_backup(queue)
333 return
okozachenkof731e622021-05-10 17:38:30 +0300334 if project_id not in self.project_list: self.process_non_existing_backup(queue)
335 self.openstacksdk.set_project(self.project_list[project_id])
336 backup_gen = self.openstacksdk.get_backup(queue.backup_id)
okozachenkoed4c4872021-05-10 17:58:37 +0300337
okozachenkod4284d22021-05-05 20:48:23 +0300338 if backup_gen == None:
339 # TODO(Alex): need to check when it is none
okozachenko179cd572021-05-06 22:23:53 +0300340 LOG.info(_("[Beta] Backup status of %s is returning none." % (queue.backup_id)))
okozachenko2ab48e82021-05-07 16:55:11 +0300341 self.process_non_existing_backup(queue)
okozachenkod4284d22021-05-05 20:48:23 +0300342 return
343 if backup_gen.status == "error":
344 self.process_failed_backup(queue)
345 elif backup_gen.status == "available":
346 self.process_available_backup(queue)
347 elif backup_gen.status == "creating":
okozachenkod4284d22021-05-05 20:48:23 +0300348 LOG.info("Waiting for backup of %s to be completed" % queue.volume_id)
349 else: # "deleting", "restoring", "error_restoring" status
350 self.process_using_backup(queue)
okozachenkod2801362021-05-05 21:23:46 +0300351 except OpenstackResourceNotFound as e:
okozachenko179cd572021-05-06 22:23:53 +0300352 self.process_non_existing_backup(queue)
okozachenko6d277e32021-05-05 20:23:32 +0300353
354 def _volume_backup(self, task):
355 # matching_backups = [
356 # g for g in self.available_backups if g.backup_id == task.backup_id
357 # ]
358 # if not matching_backups:
359 volume_backup = objects.Volume(self.ctx)
360 volume_backup.backup_id = task.backup_id
361 volume_backup.volume_id = task.volume_id
362 volume_backup.instance_id = task.instance_id
Susanta Gautamc4061112021-05-10 02:39:23 +0000363 volume_backup.project_id = task.project_id
okozachenko6d277e32021-05-05 20:23:32 +0300364 volume_backup.backup_completed = task.backup_completed
365 volume_backup.create()