blob: 53190359ef07d3f70cd756d6ec4d8996c86217bb [file] [log] [blame]
ricoline884f122024-11-01 16:28:13 +08001from __future__ import annotations
okozachenko1203ef49f952022-05-16 22:27:28 +10002
ricoline884f122024-11-01 16:28:13 +08003import collections
4from datetime import timedelta
5from datetime import timezone
6
okozachenko1203ef49f952022-05-16 22:27:28 +10007from openstack.exceptions import HttpException as OpenstackHttpException
okozachenkod2801362021-05-05 21:23:46 +03008from openstack.exceptions import ResourceNotFound as OpenstackResourceNotFound
9from openstack.exceptions import SDKException as OpenstackSDKException
okozachenko6d277e32021-05-05 20:23:32 +030010from oslo_log import log
ricolin72c3ce82023-03-27 15:08:23 +080011from oslo_utils import timeutils
ricoline884f122024-11-01 16:28:13 +080012
13from staffeln.common import constants
14from staffeln.common import context
15from staffeln.common import openstack
ricolin31181aa2023-07-18 13:51:26 +080016from staffeln.common import time as xtime
okozachenko1203d306c652022-05-16 22:44:49 +100017from staffeln.conductor import result
ricoline884f122024-11-01 16:28:13 +080018import staffeln.conf
okozachenko6d277e32021-05-05 20:23:32 +030019from staffeln.i18n import _
ricoline884f122024-11-01 16:28:13 +080020from staffeln import objects
okozachenko6d277e32021-05-05 20:23:32 +030021
22CONF = staffeln.conf.CONF
23LOG = log.getLogger(__name__)
24
25BackupMapping = collections.namedtuple(
okozachenko1203fa747f22022-05-16 20:13:54 +100026 "BackupMapping",
okozachenko1203a6f83422022-11-02 02:23:54 +110027 [
28 "volume_id",
29 "backup_id",
30 "project_id",
31 "instance_id",
32 "backup_completed",
33 "incremental",
ricolin031f06b2022-11-13 09:18:57 +080034 "created_at",
okozachenko1203a6f83422022-11-02 02:23:54 +110035 ],
okozachenko6d277e32021-05-05 20:23:32 +030036)
37
38QueueMapping = collections.namedtuple(
okozachenko1203fa747f22022-05-16 20:13:54 +100039 "QueueMapping",
Rico Lin595c41c2022-06-14 21:05:27 +080040 [
41 "volume_id",
42 "backup_id",
43 "project_id",
44 "instance_id",
45 "backup_status",
46 "instance_name",
47 "volume_name",
okozachenko1203a6f83422022-11-02 02:23:54 +110048 "incremental",
ricolin988159a2022-11-08 07:01:27 +080049 "reason",
Rico Lin595c41c2022-06-14 21:05:27 +080050 ],
okozachenko6d277e32021-05-05 20:23:32 +030051)
52
okozachenko6d277e32021-05-05 20:23:32 +030053
okozachenko12035fd8a302022-04-07 01:55:29 +100054def retry_auth(func):
55 """Decorator to reconnect openstack and avoid token rotation"""
okozachenko1203fa747f22022-05-16 20:13:54 +100056
okozachenko12035fd8a302022-04-07 01:55:29 +100057 def wrapper(self, *args, **kwargs):
58 try:
59 return func(self, *args, **kwargs)
60 except OpenstackHttpException as ex:
61 if ex.status_code == 403:
62 LOG.warn(_("Token has been expired or rotated!"))
63 self.refresh_openstacksdk()
64 return func(self, *args, **kwargs)
okozachenko1203fa747f22022-05-16 20:13:54 +100065
okozachenko12035fd8a302022-04-07 01:55:29 +100066 return wrapper
okozachenko6d277e32021-05-05 20:23:32 +030067
68
okozachenko6d277e32021-05-05 20:23:32 +030069class Backup(object):
70 """Implmentations of the queue with the sql."""
71
72 def __init__(self):
73 self.ctx = context.make_context()
okozachenko12035fd8a302022-04-07 01:55:29 +100074 self.refresh_openstacksdk()
ricolin1fe99132023-02-27 14:13:56 +080075 self.result = result.BackupResult(self)
okozachenkof731e622021-05-10 17:38:30 +030076 self.project_list = {}
okozachenkocb316082021-05-07 21:30:39 +030077
okozachenko12035fd8a302022-04-07 01:55:29 +100078 def refresh_openstacksdk(self):
79 self.openstacksdk = openstack.OpenstackSDK()
80
ricolin86576d12023-01-28 17:27:54 +080081 def publish_backup_result(self, purge_on_success=False):
82 for project_id, project_name in self.result.project_list:
83 try:
84 publish_result = self.result.publish(project_id, project_name)
85 if publish_result and purge_on_success:
86 # Purge backup queue tasks
87 self.purge_backups(project_id)
88 except Exception as ex: # pylint: disable=W0703
89 LOG.warn(
90 "Failed to publish backup result or "
91 f"purge backup tasks for project {project_id} "
92 f"{str(ex)}"
93 )
okozachenko6d277e32021-05-05 20:23:32 +030094
okozachenko9eae01d2021-05-11 16:47:27 +030095 def refresh_backup_result(self):
96 self.result.initialize()
97
okozachenko1203a6f83422022-11-02 02:23:54 +110098 def get_backups(self, filters=None, **kwargs):
okozachenko12034aa89292022-05-17 01:08:02 +100099 return objects.Volume.list( # pylint: disable=E1120
okozachenko1203a6f83422022-11-02 02:23:54 +1100100 context=self.ctx, filters=filters, **kwargs
okozachenko12034aa89292022-05-17 01:08:02 +1000101 )
okozachenko6d277e32021-05-05 20:23:32 +0300102
okozachenkofb385602021-05-07 19:00:47 +0300103 def get_backup_quota(self, project_id):
okozachenkof731e622021-05-10 17:38:30 +0300104 return self.openstacksdk.get_backup_quota(project_id)
okozachenkofb385602021-05-07 19:00:47 +0300105
ricolinf81b0db2023-03-30 13:56:58 +0800106 def get_backup_gigabytes_quota(self, project_id):
107 return self.openstacksdk.get_backup_gigabytes_quota(project_id)
108
okozachenko6d277e32021-05-05 20:23:32 +0300109 def get_queues(self, filters=None):
110 """Get the list of volume queue columns from the queue_data table"""
okozachenko12034aa89292022-05-17 01:08:02 +1000111 queues = objects.Queue.list( # pylint: disable=E1120
okozachenko120385f53e92022-05-17 00:59:29 +1000112 context=self.ctx, filters=filters
okozachenko12034aa89292022-05-17 01:08:02 +1000113 )
okozachenko6d277e32021-05-05 20:23:32 +0300114 return queues
115
ricolin4c82df02023-09-14 11:34:48 +0800116 def get_queue_task_by_id(self, task_id):
117 """Get single volume queue task from the queue_data table"""
118 queue = objects.Queue.get_by_id( # pylint: disable=E1120
119 context=self.ctx, id=task_id
120 )
121 return queue
122
okozachenko1203a6f83422022-11-02 02:23:54 +1100123 def create_queue(self, old_tasks):
ricoline884f122024-11-01 16:28:13 +0800124 """Create the queue of all the volumes for backup
okozachenko120302950702022-10-22 03:34:16 +1100125
126 :param old_tasks: Task list not completed in the previous cycle
127 :type: List<Class objects.Queue>
okozachenko120302950702022-10-22 03:34:16 +1100128 """
ricolin07db40e2023-01-02 06:37:10 +0800129
130 LOG.info("Adding new backup tasks to queue.")
okozachenkod2801362021-05-05 21:23:46 +0300131 # 1. get the old task list, not finished in the last cycle
132 # and keep till now
133 old_task_volume_list = []
134 for old_task in old_tasks:
135 old_task_volume_list.append(old_task.volume_id)
136
ricoline884f122024-11-01 16:28:13 +0800137 # 2. add new tasks in the queue which are not existing in the old task
138 # list
ricolin07db40e2023-01-02 06:37:10 +0800139 task_list = self.check_instance_volumes()
140 for task in task_list:
141 if task.volume_id not in old_task_volume_list:
142 self._volume_queue(task)
okozachenko6d277e32021-05-05 20:23:32 +0300143
144 # Backup the volumes attached to which has a specific metadata
okozachenkofb385602021-05-07 19:00:47 +0300145 def filter_by_server_metadata(self, metadata):
xuxant02@gmail.coma0cd61f2021-06-22 16:24:29 +0545146 if CONF.conductor.backup_metadata_key is not None:
okozachenko1203ef49f952022-05-16 22:27:28 +1000147 if CONF.conductor.backup_metadata_key not in metadata:
xuxant02@gmail.coma0cd61f2021-06-22 16:24:29 +0545148 return False
okozachenko6d277e32021-05-05 20:23:32 +0300149
okozachenko1203fa747f22022-05-16 20:13:54 +1000150 return (
ricoline884f122024-11-01 16:28:13 +0800151 metadata[CONF.conductor.backup_metadata_key].lower(
152 ) == constants.BACKUP_ENABLED_KEY
okozachenko1203fa747f22022-05-16 20:13:54 +1000153 )
xuxant02@gmail.coma0cd61f2021-06-22 16:24:29 +0545154 else:
Susanta Gautam6d0920f2021-06-22 16:01:24 +0545155 return True
okozachenko6d277e32021-05-05 20:23:32 +0300156
157 # Backup the volumes in in-use and available status
okozachenkocb316082021-05-07 21:30:39 +0300158 def filter_by_volume_status(self, volume_id, project_id):
okozachenko6d277e32021-05-05 20:23:32 +0300159 try:
okozachenkof731e622021-05-10 17:38:30 +0300160 volume = self.openstacksdk.get_volume(volume_id, project_id)
okozachenko1203ef49f952022-05-16 22:27:28 +1000161 if volume is None:
okozachenko1203fa747f22022-05-16 20:13:54 +1000162 return False
163 res = volume["status"] in ("available", "in-use")
okozachenko6d277e32021-05-05 20:23:32 +0300164 if not res:
okozachenko1203fa747f22022-05-16 20:13:54 +1000165 reason = _(
ricoline884f122024-11-01 16:28:13 +0800166 "Volume %s is not triger new backup task because "
167 "it is in %s status" %
168 (volume_id, volume["status"]))
okozachenkocb316082021-05-07 21:30:39 +0300169 LOG.info(reason)
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100170 return reason
okozachenko6d277e32021-05-05 20:23:32 +0300171 return res
172
okozachenkod2801362021-05-05 21:23:46 +0300173 except OpenstackResourceNotFound:
okozachenko6d277e32021-05-05 20:23:32 +0300174 return False
175
ricolin86576d12023-01-28 17:27:54 +0800176 def purge_backups(self, project_id=None):
177 LOG.info(f"Start pruge backup tasks for project {project_id}")
ricoline884f122024-11-01 16:28:13 +0800178 # We can consider make all these in a single DB command
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100179 success_tasks = self.get_queues(
ricolin86576d12023-01-28 17:27:54 +0800180 filters={
181 "backup_status": constants.BACKUP_COMPLETED,
182 "project_id": project_id,
183 }
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100184 )
185 failed_tasks = self.get_queues(
ricolin86576d12023-01-28 17:27:54 +0800186 filters={
187 "backup_status": constants.BACKUP_FAILED,
188 "project_id": project_id,
189 }
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100190 )
ricolinda471472023-05-17 21:41:39 +0800191 LOG.debug(f"Start purge completed tasks for project {project_id}")
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100192 for queue in success_tasks:
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100193 queue.delete_queue()
194
ricolinda471472023-05-17 21:41:39 +0800195 LOG.debug(f"Start purge failed tasks for project {project_id}")
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100196 for queue in failed_tasks:
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100197 queue.delete_queue()
198
ricolin3a2fad12023-01-29 17:31:30 +0800199 def create_failed_backup_obj(self, task):
200 # Create backup object for failed backups, to make sure we
201 # will review the delete later when retention triggered.
202 # Set the created_at time to retention_time ago,
203 # so it might get check on next retention cycle.
204 # For customerized retention instances, the backup might check later
205 # but the process to check the remove will eventually starts.
206 # Note: 315360000 = 10 years. The create time of an backup object will
207 # not affect report.
ricolin72c3ce82023-03-27 15:08:23 +0800208 threshold_strtime = timeutils.utcnow() - timedelta(seconds=315360000)
ricolin3a2fad12023-01-29 17:31:30 +0800209 self._volume_backup(
210 BackupMapping(
211 volume_id=task.volume_id,
212 project_id=task.project_id,
213 backup_id=task.backup_id,
214 instance_id=task.instance_id,
215 backup_completed=0,
216 incremental=task.incremental,
217 created_at=threshold_strtime,
218 )
219 )
220
okozachenkoca2b37a2021-05-06 20:38:02 +0300221 # delete all backups forcily regardless of the status
okozachenko2ab48e82021-05-07 16:55:11 +0300222 def hard_cancel_backup_task(self, task):
okozachenkoca2b37a2021-05-06 20:38:02 +0300223 try:
okozachenkof731e622021-05-10 17:38:30 +0300224 project_id = task.project_id
okozachenkocb316082021-05-07 21:30:39 +0300225 reason = _("Cancel backup %s because of timeout." % task.backup_id)
226 LOG.info(reason)
okozachenkof731e622021-05-10 17:38:30 +0300227
okozachenko1203fa747f22022-05-16 20:13:54 +1000228 if project_id not in self.project_list:
229 self.process_non_existing_backup(task)
okozachenkof731e622021-05-10 17:38:30 +0300230 self.openstacksdk.set_project(self.project_list[project_id])
231 backup = self.openstacksdk.get_backup(task.backup_id)
okozachenko1203ef49f952022-05-16 22:27:28 +1000232 if backup is None:
okozachenko1203fa747f22022-05-16 20:13:54 +1000233 return task.delete_queue()
ricolinba8e87f2023-06-03 01:24:41 +0800234 self.openstacksdk.delete_backup(task.backup_id)
ricolin3a2fad12023-01-29 17:31:30 +0800235 self.create_failed_backup_obj(task)
236
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100237 task.reason = reason
238 task.backup_status = constants.BACKUP_FAILED
239 task.save()
okozachenkoca2b37a2021-05-06 20:38:02 +0300240
241 except OpenstackSDKException as e:
ricolin8aa26282023-01-13 09:36:30 +0800242 reason = _(
243 "Backup %s deletion failed. Need delete manually: %s"
244 % (task.backup_id, str(e)[:64])
245 )
246 log_msg = _(
247 "Backup %s deletion failed. Need delete manually: %s"
248 % (task.backup_id, str(e))
249 )
ricolin75a06aa2023-01-11 18:31:16 +0800250 LOG.warn(log_msg)
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100251 task.reason = reason
252 task.backup_status = constants.BACKUP_FAILED
253 task.save()
okozachenkoca2b37a2021-05-06 20:38:02 +0300254
okozachenkof731e622021-05-10 17:38:30 +0300255 # delete only available backups: reserved
okozachenko2ab48e82021-05-07 16:55:11 +0300256 def soft_remove_backup_task(self, backup_object):
okozachenko6d277e32021-05-05 20:23:32 +0300257 try:
okozachenkof731e622021-05-10 17:38:30 +0300258 backup = self.openstacksdk.get_backup(backup_object.backup_id)
okozachenko1203ef49f952022-05-16 22:27:28 +1000259 if backup is None:
okozachenko1203fa747f22022-05-16 20:13:54 +1000260 LOG.info(
ricolind016f3e2024-11-01 16:16:50 +0800261 f"Backup {backup_object.backup_id} is removed from "
262 "Openstack or cinder-backup is not existing in the cloud."
okozachenko1203fa747f22022-05-16 20:13:54 +1000263 )
okozachenko63024952021-05-10 20:04:25 +0300264 return backup_object.delete_backup()
okozachenko6d277e32021-05-05 20:23:32 +0300265 if backup["status"] in ("available"):
okozachenkof731e622021-05-10 17:38:30 +0300266 self.openstacksdk.delete_backup(backup_object.backup_id)
ricolin082804c2023-01-28 16:02:57 +0800267 # Don't remove backup until it's officially removed from Cinder
268 # backup_object.delete_backup()
okozachenko6d277e32021-05-05 20:23:32 +0300269 elif backup["status"] in ("error", "error_restoring"):
ricolind456d082023-01-17 04:03:41 +0800270 # Try to remove it from cinder
271 self.openstacksdk.delete_backup(backup_object.backup_id)
ricolin082804c2023-01-28 16:02:57 +0800272 # Don't remove backup until it's officially removed from Cinder
273 # backup_object.delete_backup()
okozachenko6d277e32021-05-05 20:23:32 +0300274 else: # "deleting", "restoring"
okozachenko1203fa747f22022-05-16 20:13:54 +1000275 LOG.info(
ricolind016f3e2024-11-01 16:16:50 +0800276 f"Rotation for the backup {backup_object.backup_id} "
277 "is skipped in this cycle "
278 f"because it is in {backup['status']} status"
okozachenko1203fa747f22022-05-16 20:13:54 +1000279 )
okozachenko6d277e32021-05-05 20:23:32 +0300280
okozachenkoca2b37a2021-05-06 20:38:02 +0300281 except OpenstackSDKException as e:
ricoline884f122024-11-01 16:28:13 +0800282 LOG.warn(
283 f"Backup {backup_object.backup_id} deletion failed. {str(e)}"
284 )
ricolind456d082023-01-17 04:03:41 +0800285 # We don't delete backup object if any exception occured
286 # backup_object.delete_backup()
okozachenkoca2b37a2021-05-06 20:38:02 +0300287 return False
288
okozachenkoca2b37a2021-05-06 20:38:02 +0300289 # delete all backups forcily regardless of the status
ricolin5583c882023-01-11 22:06:08 +0800290 def hard_remove_volume_backup(self, backup_object, skip_inc_err=False):
okozachenkoca2b37a2021-05-06 20:38:02 +0300291 try:
okozachenkob875cbe2021-05-11 19:29:51 +0300292 project_id = backup_object.project_id
293 if project_id not in self.project_list:
ricolind456d082023-01-17 04:03:41 +0800294 LOG.warn(
ricolind016f3e2024-11-01 16:16:50 +0800295 f"Project {project_id} for backup "
296 f"{backup_object.backup_id} is not existing in "
ricoline884f122024-11-01 16:28:13 +0800297 "Openstack. Please check your access right to this "
298 "project. "
299 "Skip this backup from remove now and will retry later.")
300 # Don't remove backup object, keep it and retry on next
301 # periodic task backup_object.delete_backup()
ricolind456d082023-01-17 04:03:41 +0800302 return
okozachenkob875cbe2021-05-11 19:29:51 +0300303
okozachenko5eadfef2021-05-12 13:55:45 +0300304 self.openstacksdk.set_project(self.project_list[project_id])
okozachenko1203fa747f22022-05-16 20:13:54 +1000305 backup = self.openstacksdk.get_backup(
306 uuid=backup_object.backup_id, project_id=project_id
307 )
okozachenko1203ef49f952022-05-16 22:27:28 +1000308 if backup is None:
okozachenko1203fa747f22022-05-16 20:13:54 +1000309 LOG.info(
ricoline884f122024-11-01 16:28:13 +0800310 f"Backup {backup_object.backup_id} is removed from "
311 "Openstack or cinder-backup is not existing in the "
312 "cloud. Start removing backup object from Staffeln.")
okozachenko63024952021-05-10 20:04:25 +0300313 return backup_object.delete_backup()
okozachenkoca2b37a2021-05-06 20:38:02 +0300314
ricolinba8e87f2023-06-03 01:24:41 +0800315 self.openstacksdk.delete_backup(uuid=backup_object.backup_id)
ricolin082804c2023-01-28 16:02:57 +0800316 # Don't remove backup until it's officially removed from Cinder
317 # backup_object.delete_backup()
ricolin75a06aa2023-01-11 18:31:16 +0800318 except Exception as e:
ricoline884f122024-11-01 16:28:13 +0800319 if (
320 skip_inc_err and (
321 "Incremental backups exist for this backup" in str(e))
322 ):
ricolin082804c2023-01-28 16:02:57 +0800323 LOG.debug(str(e))
ricolin5583c882023-01-11 22:06:08 +0800324 else:
ricolin082804c2023-01-28 16:02:57 +0800325 LOG.info(
ricolind016f3e2024-11-01 16:16:50 +0800326 f"Backup {backup_object.backup_id} deletion failed. "
327 "Skip this backup from remove now and will retry later."
okozachenko1203aaeb0472022-05-17 02:28:15 +1000328 )
ricolin082804c2023-01-28 16:02:57 +0800329 LOG.debug(f"deletion failed {str(e)}")
okozachenkoa6dcdc02021-05-10 14:13:26 +0300330
ricoline884f122024-11-01 16:28:13 +0800331 # Don't remove backup object, keep it and retry on next
332 # periodic task backup_object.delete_backup()
okozachenkoca2b37a2021-05-06 20:38:02 +0300333
okozachenkob875cbe2021-05-11 19:29:51 +0300334 def update_project_list(self):
335 projects = self.openstacksdk.get_projects()
336 for project in projects:
337 self.project_list[project.id] = project
338
ricolin0c10d972023-01-14 07:10:30 +0800339 def _is_backup_required(self, volume_id):
ricoline884f122024-11-01 16:28:13 +0800340 """Decide if the backup required based on the backup history
ricolin0c10d972023-01-14 07:10:30 +0800341
342 If there is any backup created during certain time,
343 will not trigger new backup request.
344 This will judge on CONF.conductor.backup_min_interval
345
346 :param volume_id: Target volume id
347 :type: uuid string
348
349 :return: if new backup required
350 :return type: bool
351 """
352 # select * from backup order by Id;
353 try:
354 if CONF.conductor.backup_min_interval == 0:
355 # Ignore backup interval
356 return True
357 interval = CONF.conductor.backup_min_interval
ricoline884f122024-11-01 16:28:13 +0800358 threshold_strtime = timeutils.utcnow() - timedelta(
359 seconds=interval
360 )
ricolin0c10d972023-01-14 07:10:30 +0800361 backups = self.get_backups(
362 filters={
363 "volume_id__eq": volume_id,
ricoline884f122024-11-01 16:28:13 +0800364 "created_at__gt": threshold_strtime.astimezone(
365 timezone.utc
366 ),
ricolin0c10d972023-01-14 07:10:30 +0800367 }
368 )
369 if backups:
370 return False
371 except Exception as e:
372 LOG.debug(
373 _(
374 "Failed to get backup history to decide backup is "
375 "required or not. Reason: %s" % str(e)
376 )
377 )
378 return True
379
okozachenko1203a6f83422022-11-02 02:23:54 +1100380 def _is_incremental(self, volume_id):
ricoline884f122024-11-01 16:28:13 +0800381 """Decide the backup method based on the backup history
okozachenko1203a6f83422022-11-02 02:23:54 +1100382
383 It queries to select the last N backups from backup table and
384 decide backup type as full if there is no full backup.
385 N equals to CONF.conductor.full_backup_depth.
386
387 :param volume_id: Target volume id
388 :type: uuid string
389
390 :return: if backup method is incremental or not
391 :return type: bool
392 """
ricolin415281c2022-12-29 03:44:34 +0800393 # select * from backup order by Id DESC LIMIT 2;
okozachenko1203a6f83422022-11-02 02:23:54 +1100394 try:
ricolin7de4b0a2023-01-13 09:29:09 +0800395 if CONF.conductor.full_backup_depth == 0:
396 return False
okozachenko1203a6f83422022-11-02 02:23:54 +1100397 backups = self.get_backups(
398 filters={"volume_id__eq": volume_id},
399 limit=CONF.conductor.full_backup_depth,
400 sort_key="id",
401 sort_dir="desc",
402 )
403 for bk in backups:
404 if bk.incremental:
405 continue
406 else:
407 return True
408 except Exception as e:
409 LOG.debug(
ricoline884f122024-11-01 16:28:13 +0800410 "Failed to get backup history to decide backup "
411 f"method. Reason: {e}"
okozachenko1203a6f83422022-11-02 02:23:54 +1100412 )
413 return False
414
okozachenko6d277e32021-05-05 20:23:32 +0300415 def check_instance_volumes(self):
ricoline884f122024-11-01 16:28:13 +0800416 """Retrieves volume list to backup
okozachenko120302950702022-10-22 03:34:16 +1100417
418 Get the list of all the volumes from the project using openstacksdk.
okozachenko6d277e32021-05-05 20:23:32 +0300419 Function first list all the servers in the project and get the volumes
420 that are attached to the instance.
ricolin0c10d972023-01-14 07:10:30 +0800421
422 Generate backup candidate list for later create tasks in queue
okozachenko6d277e32021-05-05 20:23:32 +0300423 """
424 queues_map = []
okozachenko12033b2f3542022-05-16 19:33:46 +1000425 self.refresh_openstacksdk()
okozachenkof731e622021-05-10 17:38:30 +0300426 projects = self.openstacksdk.get_projects()
okozachenko6d277e32021-05-05 20:23:32 +0300427 for project in projects:
okozachenkocb316082021-05-07 21:30:39 +0300428 empty_project = True
okozachenkof731e622021-05-10 17:38:30 +0300429 self.project_list[project.id] = project
okozachenko12033b2f3542022-05-16 19:33:46 +1000430 try:
431 servers = self.openstacksdk.get_servers(project_id=project.id)
432 except OpenstackHttpException as ex:
okozachenko1203fa747f22022-05-16 20:13:54 +1000433 LOG.warn(
ricoline884f122024-11-01 16:28:13 +0800434 f"Failed to list servers in project {project.id}. "
435 f"{str(ex)} (status code: {ex.status_code})."
okozachenko1203fa747f22022-05-16 20:13:54 +1000436 )
okozachenko12033b2f3542022-05-16 19:33:46 +1000437 continue
okozachenko6d277e32021-05-05 20:23:32 +0300438 for server in servers:
okozachenko1203fa747f22022-05-16 20:13:54 +1000439 if not self.filter_by_server_metadata(server.metadata):
440 continue
okozachenkocb316082021-05-07 21:30:39 +0300441 if empty_project:
442 empty_project = False
443 self.result.add_project(project.id, project.name)
okozachenko6d277e32021-05-05 20:23:32 +0300444 for volume in server.attached_volumes:
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100445 filter_result = self.filter_by_volume_status(
ricolin66a0a9d2022-11-07 07:49:31 +0800446 volume["id"], project.id
447 )
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100448
449 if not filter_result:
okozachenko1203fa747f22022-05-16 20:13:54 +1000450 continue
ricolin0c10d972023-01-14 07:10:30 +0800451 backup_required = self._is_backup_required(volume["id"])
452 if not backup_required:
453 continue
454
okozachenko1203a6f83422022-11-02 02:23:54 +1100455 if "name" not in volume or not volume["name"]:
okozachenko1203a95bdcf2022-07-08 00:29:12 +1000456 volume_name = volume["id"]
457 else:
458 volume_name = volume["name"][:100]
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100459 if filter_result is True:
460 backup_status = constants.BACKUP_PLANNED
461 reason = None
462 else:
463 backup_status = constants.BACKUP_FAILED
464 reason = filter_result
ricolinea4d82a2023-01-03 06:25:34 +0800465 incremental = self._is_incremental(volume["id"])
ricolinccb497d2023-01-02 07:51:57 +0800466 backup_method = "Incremental" if incremental else "Full"
ricolin65367962023-01-02 08:55:41 +0800467 LOG.info(
ricolinccb497d2023-01-02 07:51:57 +0800468 "Prapering %s backup task for volume %s",
469 backup_method,
ricolinea4d82a2023-01-03 06:25:34 +0800470 volume["id"],
ricolinccb497d2023-01-02 07:51:57 +0800471 )
okozachenko6d277e32021-05-05 20:23:32 +0300472 queues_map.append(
473 QueueMapping(
okozachenko179cd572021-05-06 22:23:53 +0300474 project_id=project.id,
okozachenko6d277e32021-05-05 20:23:32 +0300475 volume_id=volume["id"],
476 backup_id="NULL",
477 instance_id=server.id,
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100478 backup_status=backup_status,
Rico Lin595c41c2022-06-14 21:05:27 +0800479 # Only keep the last 100 chars of instance_name and
480 # volume_name for forming backup_name
481 instance_name=server.name[:100],
okozachenko1203a95bdcf2022-07-08 00:29:12 +1000482 volume_name=volume_name,
ricolinccb497d2023-01-02 07:51:57 +0800483 incremental=incremental,
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100484 reason=reason,
okozachenko6d277e32021-05-05 20:23:32 +0300485 )
486 )
487 return queues_map
488
ricolin031f06b2022-11-13 09:18:57 +0800489 def collect_instance_retention_map(self):
490 """Retrieves instance backup retention map"""
491
492 retention_map = {}
493 # No customized retention.
494 if not CONF.conductor.retention_metadata_key:
495 return retention_map
496 self.refresh_openstacksdk()
497
498 try:
499 servers = self.openstacksdk.get_servers(all_projects=True)
ricoline884f122024-11-01 16:28:13 +0800500 except OpenstackHttpException as ex:
501 servers = []
502 LOG.warn(
503 f"Failed to list servers for all projects. "
504 f"{str(ex)} (status code: {ex.status_code})."
505 )
ricolin031f06b2022-11-13 09:18:57 +0800506
507 for server in servers:
508 if CONF.conductor.retention_metadata_key in server.metadata:
ricolin31181aa2023-07-18 13:51:26 +0800509 server_retention_time = server.metadata[
ricolin031f06b2022-11-13 09:18:57 +0800510 CONF.conductor.retention_metadata_key
511 ].lower()
ricolin31181aa2023-07-18 13:51:26 +0800512 if xtime.regex.fullmatch(server_retention_time):
513 LOG.debug(
ricoline884f122024-11-01 16:28:13 +0800514 f"Found retention time ({server_retention_time}) "
515 f"defined for server {server.id}, "
516 "Adding it retention reference map.")
ricolin31181aa2023-07-18 13:51:26 +0800517 retention_map[server.id] = server_retention_time
518 else:
519 LOG.info(
ricoline884f122024-11-01 16:28:13 +0800520 f"Server retention time for instance {server.id} is "
521 "incorrect. Please follow "
522 "'<YEARS>y<MONTHS>m<WEEKS>w<DAYS>d<HOURS>"
523 "h<MINUTES>min<SECONDS>s' format.")
ricolin031f06b2022-11-13 09:18:57 +0800524 return retention_map
525
okozachenko1203a6f83422022-11-02 02:23:54 +1100526 def _volume_queue(self, task):
ricoline884f122024-11-01 16:28:13 +0800527 """Commits one backup task to queue table
okozachenko120302950702022-10-22 03:34:16 +1100528
529 :param task: One backup task
530 :type: QueueMapping
okozachenko120302950702022-10-22 03:34:16 +1100531 """
okozachenko6d277e32021-05-05 20:23:32 +0300532 volume_queue = objects.Queue(self.ctx)
533 volume_queue.backup_id = task.backup_id
534 volume_queue.volume_id = task.volume_id
535 volume_queue.instance_id = task.instance_id
Susanta Gautamc4061112021-05-10 02:39:23 +0000536 volume_queue.project_id = task.project_id
okozachenko6d277e32021-05-05 20:23:32 +0300537 volume_queue.backup_status = task.backup_status
Rico Lin595c41c2022-06-14 21:05:27 +0800538 volume_queue.instance_name = task.instance_name
539 volume_queue.volume_name = task.volume_name
okozachenko120302950702022-10-22 03:34:16 +1100540 # NOTE(Oleks): Backup mode is inherited from backup service.
ricoline884f122024-11-01 16:28:13 +0800541 # Need to keep and navigate backup mode history, to decide a different
542 # mode per volume
okozachenko1203a6f83422022-11-02 02:23:54 +1100543 volume_queue.incremental = task.incremental
ricolin415281c2022-12-29 03:44:34 +0800544
545 backup_method = "Incremental" if task.incremental else "Full"
546 LOG.info(
547 _(
548 ("Schedule %s backup task for volume %s.")
549 % (backup_method, task.volume_id)
550 )
551 )
okozachenko1203a6f83422022-11-02 02:23:54 +1100552 return volume_queue.create()
okozachenko6d277e32021-05-05 20:23:32 +0300553
okozachenko120302950702022-10-22 03:34:16 +1100554 def create_volume_backup(self, task):
okozachenko6d277e32021-05-05 20:23:32 +0300555 """Initiate the backup of the volume
ricoline884f122024-11-01 16:28:13 +0800556
okozachenko120302950702022-10-22 03:34:16 +1100557 :param task: Provide the map of the volume that needs
okozachenko6d277e32021-05-05 20:23:32 +0300558 backup.
559 This function will call the backupup api and change the
okozachenko120302950702022-10-22 03:34:16 +1100560 backup_status and backup_id in the task queue table.
okozachenko6d277e32021-05-05 20:23:32 +0300561 """
okozachenko120302950702022-10-22 03:34:16 +1100562 project_id = task.project_id
ricolin72c3ce82023-03-27 15:08:23 +0800563 timestamp = int(timeutils.utcnow().timestamp())
Rico Lin595c41c2022-06-14 21:05:27 +0800564 # Backup name allows max 255 chars of string
okozachenko1203a95bdcf2022-07-08 00:29:12 +1000565 backup_name = ("%(instance_name)s_%(volume_name)s_%(timestamp)s") % {
okozachenko120302950702022-10-22 03:34:16 +1100566 "instance_name": task.instance_name,
567 "volume_name": task.volume_name,
Rico Lin595c41c2022-06-14 21:05:27 +0800568 "timestamp": timestamp,
569 }
570
571 # Make sure we don't exceed max size of backup_name
572 backup_name = backup_name[:255]
okozachenko120302950702022-10-22 03:34:16 +1100573 if task.backup_id == "NULL":
okozachenko6d277e32021-05-05 20:23:32 +0300574 try:
okozachenko179cd572021-05-06 22:23:53 +0300575 # NOTE(Alex): no need to wait because we have a cycle time out
okozachenko1203d22dd2d2021-06-08 16:14:02 +0200576 if project_id not in self.project_list:
okozachenko1203fa747f22022-05-16 20:13:54 +1000577 LOG.warn(
ricoline884f122024-11-01 16:28:13 +0800578 _(
579 "Project ID %s is not existing in project list"
580 % project_id
581 )
okozachenko1203fa747f22022-05-16 20:13:54 +1000582 )
okozachenko120302950702022-10-22 03:34:16 +1100583 self.process_non_existing_backup(task)
okozachenko1203d22dd2d2021-06-08 16:14:02 +0200584 return
okozachenkof731e622021-05-10 17:38:30 +0300585 self.openstacksdk.set_project(self.project_list[project_id])
okozachenko1203a6f83422022-11-02 02:23:54 +1100586 backup_method = "Incremental" if task.incremental else "Full"
okozachenko1203fa747f22022-05-16 20:13:54 +1000587 LOG.info(
588 _(
ricoline884f122024-11-01 16:28:13 +0800589 (
590 "%s Backup (name: %s) for volume %s creating "
591 "in project %s"
592 )
593 % (
594 backup_method,
595 backup_name,
596 task.volume_id,
597 project_id,
598 )
okozachenko1203fa747f22022-05-16 20:13:54 +1000599 )
600 )
601 volume_backup = self.openstacksdk.create_backup(
okozachenko120302950702022-10-22 03:34:16 +1100602 volume_id=task.volume_id,
Rico Lin595c41c2022-06-14 21:05:27 +0800603 project_id=project_id,
604 name=backup_name,
okozachenko1203a6f83422022-11-02 02:23:54 +1100605 incremental=task.incremental,
okozachenko1203fa747f22022-05-16 20:13:54 +1000606 )
okozachenko120302950702022-10-22 03:34:16 +1100607 task.backup_id = volume_backup.id
ricolinb9e10b62023-01-03 05:32:35 +0800608 task.backup_status = constants.BACKUP_WIP
609 task.save()
okozachenkod2801362021-05-05 21:23:46 +0300610 except OpenstackSDKException as error:
ricoline884f122024-11-01 16:28:13 +0800611 inc_err_msg = (
612 "No backups available to do an incremental backup"
613 )
ricolin75a06aa2023-01-11 18:31:16 +0800614 if inc_err_msg in str(error):
615 LOG.info(
ricoline884f122024-11-01 16:28:13 +0800616 "Retry to create full backup for volume %s instead of "
617 "incremental." %
618 task.volume_id)
ricolin75a06aa2023-01-11 18:31:16 +0800619 task.incremental = False
620 task.save()
621 else:
622 reason = _(
ricoline884f122024-11-01 16:28:13 +0800623 "Backup (name: %s) creation for the volume %s "
624 "failled. %s" %
625 (backup_name, task.volume_id, str(error)[
626 :64]))
ricolin75a06aa2023-01-11 18:31:16 +0800627 LOG.warn(
ricoline884f122024-11-01 16:28:13 +0800628 "Backup (name: %s) creation for the volume %s "
629 "failled. %s" %
630 (backup_name, task.volume_id, str(error)))
ricolin75a06aa2023-01-11 18:31:16 +0800631 task.reason = reason
632 task.backup_status = constants.BACKUP_FAILED
633 task.save()
ricoline884f122024-11-01 16:28:13 +0800634 # Added extra exception as OpenstackSDKException does not handle
635 # the keystone unauthourized issue.
xuxant02@gmail.com79b43972021-06-21 21:36:57 +0545636 except Exception as error:
okozachenko1203fa747f22022-05-16 20:13:54 +1000637 reason = _(
Rico Lin595c41c2022-06-14 21:05:27 +0800638 "Backup (name: %s) creation for the volume %s failled. %s"
ricolin1d81a472023-01-10 05:12:10 +0800639 % (backup_name, task.volume_id, str(error)[:64])
okozachenko1203fa747f22022-05-16 20:13:54 +1000640 )
ricolin75a06aa2023-01-11 18:31:16 +0800641 LOG.warn(
642 "Backup (name: %s) creation for the volume %s failled. %s"
643 % (backup_name, task.volume_id, str(error))
644 )
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100645 task.reason = reason
646 task.backup_status = constants.BACKUP_FAILED
647 task.save()
okozachenko6d277e32021-05-05 20:23:32 +0300648 else:
okozachenko12033b2f3542022-05-16 19:33:46 +1000649 # Backup planned task cannot have backup_id in the same cycle.
650 # Remove this task from the task list
okozachenko120302950702022-10-22 03:34:16 +1100651 task.delete_queue()
okozachenko6d277e32021-05-05 20:23:32 +0300652
okozachenkoa6dcdc02021-05-10 14:13:26 +0300653 # backup gen was not created
654 def process_pre_failed_backup(self, task):
655 # 1.notify via email
okozachenko1203fa747f22022-05-16 20:13:54 +1000656 reason = _(
ricoline884f122024-11-01 16:28:13 +0800657 "The backup creation for the volume %s was prefailed."
658 % task.volume_id
okozachenko1203fa747f22022-05-16 20:13:54 +1000659 )
okozachenko1203aaeb0472022-05-17 02:28:15 +1000660 LOG.warn(reason)
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100661 task.reason = reason
662 task.backup_status = constants.BACKUP_FAILED
663 task.save()
okozachenkoa6dcdc02021-05-10 14:13:26 +0300664
okozachenko6d277e32021-05-05 20:23:32 +0300665 def process_failed_backup(self, task):
okozachenkof731e622021-05-10 17:38:30 +0300666 # 1. notify via email
ricoline884f122024-11-01 16:28:13 +0800667 reason = (
668 f"The status of backup for the volume {task.volume_id} is error."
669 )
okozachenko1203aaeb0472022-05-17 02:28:15 +1000670 LOG.warn(reason)
okozachenkof731e622021-05-10 17:38:30 +0300671 # 2. delete backup generator
okozachenko1203aaeb0472022-05-17 02:28:15 +1000672 try:
ricolinba8e87f2023-06-03 01:24:41 +0800673 self.openstacksdk.delete_backup(uuid=task.backup_id)
ricolin3a2fad12023-01-29 17:31:30 +0800674 self.create_failed_backup_obj(task)
okozachenko1203aaeb0472022-05-17 02:28:15 +1000675 except OpenstackHttpException as ex:
ricolin75a06aa2023-01-11 18:31:16 +0800676 LOG.warn(
okozachenko1203aaeb0472022-05-17 02:28:15 +1000677 _(
ricolin7a5582f2024-11-01 21:11:14 +0800678 "Failed to delete volume backup %s. %s. Need "
679 "to delete manually." % (task.backup_id, str(ex))
okozachenko1203aaeb0472022-05-17 02:28:15 +1000680 )
681 )
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100682 task.reason = reason
683 task.backup_status = constants.BACKUP_FAILED
684 task.save()
okozachenko6d277e32021-05-05 20:23:32 +0300685
okozachenko179cd572021-05-06 22:23:53 +0300686 def process_non_existing_backup(self, task):
687 task.delete_queue()
688
okozachenko6d277e32021-05-05 20:23:32 +0300689 def process_available_backup(self, task):
690 LOG.info("Backup of the volume %s is successful." % task.volume_id)
691 # 1. save success backup in the backup table
692 self._volume_backup(
693 BackupMapping(
694 volume_id=task.volume_id,
okozachenko179cd572021-05-06 22:23:53 +0300695 project_id=task.project_id,
okozachenko6d277e32021-05-05 20:23:32 +0300696 backup_id=task.backup_id,
697 instance_id=task.instance_id,
698 backup_completed=1,
okozachenko1203a6f83422022-11-02 02:23:54 +1100699 incremental=task.incremental,
ricolin72c3ce82023-03-27 15:08:23 +0800700 created_at=timeutils.utcnow(),
okozachenko6d277e32021-05-05 20:23:32 +0300701 )
702 )
Oleksandr Kozachenko9685c152022-11-02 14:19:18 +0100703 task.backup_status = constants.BACKUP_COMPLETED
704 task.save()
okozachenko6d277e32021-05-05 20:23:32 +0300705
706 def process_using_backup(self, task):
okozachenkocb316082021-05-07 21:30:39 +0300707 # treat same as the available backup for now
708 self.process_available_backup(task)
okozachenko6d277e32021-05-05 20:23:32 +0300709
710 def check_volume_backup_status(self, queue):
711 """Checks the backup status of the volume
ricoline884f122024-11-01 16:28:13 +0800712
okozachenko6d277e32021-05-05 20:23:32 +0300713 :params: queue: Provide the map of the volume that needs backup
714 status checked.
715 Call the backups api to see if the backup is successful.
716 """
okozachenko63024952021-05-10 20:04:25 +0300717 project_id = queue.project_id
okozachenkoed4c4872021-05-10 17:58:37 +0300718
okozachenko63024952021-05-10 20:04:25 +0300719 # The case in which the error produced before backup gen created.
720 if queue.backup_id == "NULL":
721 self.process_pre_failed_backup(queue)
722 return
okozachenko1203fa747f22022-05-16 20:13:54 +1000723 if project_id not in self.project_list:
724 self.process_non_existing_backup(queue)
okozachenko1203aaeb0472022-05-17 02:28:15 +1000725 return
okozachenko63024952021-05-10 20:04:25 +0300726 self.openstacksdk.set_project(self.project_list[project_id])
727 backup_gen = self.openstacksdk.get_backup(queue.backup_id)
okozachenkoed4c4872021-05-10 17:58:37 +0300728
okozachenko1203ef49f952022-05-16 22:27:28 +1000729 if backup_gen is None:
okozachenko63024952021-05-10 20:04:25 +0300730 # TODO(Alex): need to check when it is none
okozachenko1203fa747f22022-05-16 20:13:54 +1000731 LOG.info(
ricoline884f122024-11-01 16:28:13 +0800732 _(
733 "[Beta] Backup status of %s is returning none."
734 % (queue.backup_id)
735 )
okozachenko1203fa747f22022-05-16 20:13:54 +1000736 )
okozachenko179cd572021-05-06 22:23:53 +0300737 self.process_non_existing_backup(queue)
okozachenko63024952021-05-10 20:04:25 +0300738 return
739 if backup_gen.status == "error":
740 self.process_failed_backup(queue)
741 elif backup_gen.status == "available":
742 self.process_available_backup(queue)
743 elif backup_gen.status == "creating":
ricoline884f122024-11-01 16:28:13 +0800744 LOG.info(
745 "Waiting for backup of %s to be completed" % queue.volume_id
746 )
okozachenko63024952021-05-10 20:04:25 +0300747 else: # "deleting", "restoring", "error_restoring" status
748 self.process_using_backup(queue)
749
okozachenko6d277e32021-05-05 20:23:32 +0300750 def _volume_backup(self, task):
751 # matching_backups = [
ricoline884f122024-11-01 16:28:13 +0800752 # g for g in self.available_backups
753 # if g.backup_id == task.backup_id
okozachenko6d277e32021-05-05 20:23:32 +0300754 # ]
755 # if not matching_backups:
756 volume_backup = objects.Volume(self.ctx)
757 volume_backup.backup_id = task.backup_id
758 volume_backup.volume_id = task.volume_id
759 volume_backup.instance_id = task.instance_id
Susanta Gautamc4061112021-05-10 02:39:23 +0000760 volume_backup.project_id = task.project_id
okozachenko6d277e32021-05-05 20:23:32 +0300761 volume_backup.backup_completed = task.backup_completed
okozachenko1203a6f83422022-11-02 02:23:54 +1100762 volume_backup.incremental = task.incremental
okozachenko6d277e32021-05-05 20:23:32 +0300763 volume_backup.create()