Coverage for models / lox_backup_log.py: 32%
120 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-28 01:16 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-28 01:16 +0000
1# -*- coding: utf-8 -*-
3from odoo import models, fields, api, _
4from odoo.exceptions import UserError
5import logging
7_logger = logging.getLogger(__name__)
10class LoxBackupLog(models.Model):
11 _name = 'lox.backup.log'
12 _description = 'LOX Backup Log'
13 _order = 'create_date desc'
14 _rec_name = 'display_name'
16 config_id = fields.Many2one(
17 'lox.backup.config',
18 string='Configuration',
19 required=True,
20 ondelete='cascade',
21 )
22 schedule_id = fields.Many2one(
23 'lox.backup.schedule',
24 string='Schedule',
25 ondelete='set null',
26 )
27 backup_uuid = fields.Char(
28 string='Backup UUID',
29 readonly=True,
30 index=True,
31 )
32 display_name = fields.Char(
33 string='Name',
34 compute='_compute_display_name',
35 store=True,
36 )
38 component = fields.Selection([
39 ('full', 'Full Backup'),
40 ('database', 'Database Only'),
41 ('filestore', 'Filestore Only'),
42 ], string='Component', default='full')
44 status = fields.Selection([
45 ('pending', 'Pending'),
46 ('in_progress', 'In Progress'),
47 ('validating', 'Validating'),
48 ('completed', 'Completed'),
49 ('failed', 'Failed'),
50 ('cancelled', 'Cancelled'),
51 ], string='Status', default='pending', required=True, index=True)
53 size_bytes = fields.Float(
54 string='Size (bytes)',
55 readonly=True,
56 )
57 size_display = fields.Char(
58 string='Size',
59 compute='_compute_size_display',
60 )
62 started_at = fields.Datetime(
63 string='Started At',
64 readonly=True,
65 )
66 completed_at = fields.Datetime(
67 string='Completed At',
68 readonly=True,
69 )
70 duration = fields.Float(
71 string='Duration (s)',
72 compute='_compute_duration',
73 store=True,
74 )
76 error_message = fields.Text(
77 string='Error Message',
78 readonly=True,
79 )
81 # Storage info
82 storage_path = fields.Char(
83 string='Storage Path',
84 readonly=True,
85 )
86 checksum = fields.Char(
87 string='Checksum',
88 readonly=True,
89 )
91 # Restore tracking
92 restore_requested = fields.Boolean(
93 string='Restore Requested',
94 default=False,
95 )
96 restore_status = fields.Selection([
97 ('none', 'Not Requested'),
98 ('pending', 'Pending'),
99 ('in_progress', 'In Progress'),
100 ('ready', 'Ready for Download'),
101 ('completed', 'Restored'),
102 ('failed', 'Failed'),
103 ], string='Restore Status', default='none')
104 restore_url = fields.Char(
105 string='Restore URL',
106 readonly=True,
107 )
108 restore_expires_at = fields.Datetime(
109 string='Restore Expires At',
110 readonly=True,
111 )
113 @api.depends('backup_uuid', 'create_date')
114 def _compute_display_name(self):
115 for record in self:
116 date_str = record.create_date.strftime('%Y-%m-%d %H:%M') if record.create_date else 'New'
117 uuid_short = record.backup_uuid[:8] if record.backup_uuid else 'pending'
118 record.display_name = f'{date_str} [{uuid_short}]'
120 @api.depends('size_bytes')
121 def _compute_size_display(self):
122 for record in self:
123 if not record.size_bytes:
124 record.size_display = '-'
125 elif record.size_bytes < 1024:
126 record.size_display = f'{record.size_bytes:.0f} B'
127 elif record.size_bytes < 1024 * 1024:
128 record.size_display = f'{record.size_bytes / 1024:.1f} KB'
129 elif record.size_bytes < 1024 * 1024 * 1024:
130 record.size_display = f'{record.size_bytes / (1024 * 1024):.1f} MB'
131 else:
132 record.size_display = f'{record.size_bytes / (1024 * 1024 * 1024):.2f} GB'
134 @api.depends('started_at', 'completed_at')
135 def _compute_duration(self):
136 for record in self:
137 if record.started_at and record.completed_at:
138 delta = record.completed_at - record.started_at
139 record.duration = delta.total_seconds()
140 else:
141 record.duration = 0
143 def action_refresh_status(self):
144 """Refresh backup status from LOX API"""
145 self.ensure_one()
146 if not self.backup_uuid:
147 raise UserError(_('No backup UUID available.'))
149 api = self.env['lox.api'].create_client(self.config_id)
151 try:
152 result = api.get_backup_status(self.backup_uuid)
154 status_mapping = {
155 'PENDING': 'pending',
156 'UPLOADING': 'in_progress',
157 'VALIDATING': 'validating',
158 'COMPLETED': 'completed',
159 'FAILED': 'failed',
160 'CANCELLED': 'cancelled',
161 }
163 updates = {}
164 if result.get('status'):
165 updates['status'] = status_mapping.get(result['status'], 'pending')
166 if result.get('size'):
167 updates['size_bytes'] = result['size']
168 if result.get('checksum'):
169 updates['checksum'] = result['checksum']
170 if result.get('completed_at'):
171 updates['completed_at'] = result['completed_at']
172 if result.get('error'):
173 updates['error_message'] = result['error']
175 if updates:
176 self.write(updates)
178 return {
179 'type': 'ir.actions.client',
180 'tag': 'display_notification',
181 'params': {
182 'title': _('Status Updated'),
183 'message': _('Backup status: %s') % self.status,
184 'type': 'info',
185 'sticky': False,
186 }
187 }
188 except Exception as e:
189 raise UserError(_('Failed to refresh status: %s') % str(e))
191 def action_request_restore(self):
192 """Request restore/download of this backup"""
193 self.ensure_one()
194 if not self.backup_uuid:
195 raise UserError(_('No backup UUID available.'))
197 if self.status != 'completed':
198 raise UserError(_('Only completed backups can be restored.'))
200 api = self.env['lox.api'].create_client(self.config_id)
202 try:
203 result = api.request_restore(self.backup_uuid)
205 self.write({
206 'restore_requested': True,
207 'restore_status': 'pending',
208 })
210 return {
211 'type': 'ir.actions.client',
212 'tag': 'display_notification',
213 'params': {
214 'title': _('Restore Requested'),
215 'message': _('Restore request submitted. Check back soon for download link.'),
216 'type': 'success',
217 'sticky': False,
218 }
219 }
220 except Exception as e:
221 raise UserError(_('Failed to request restore: %s') % str(e))
223 def action_get_download_url(self):
224 """Get download URL for restored backup"""
225 self.ensure_one()
226 if not self.backup_uuid:
227 raise UserError(_('No backup UUID available.'))
229 api = self.env['lox.api'].create_client(self.config_id)
231 try:
232 result = api.get_download_url(self.backup_uuid)
234 if result.get('url'):
235 self.write({
236 'restore_url': result['url'],
237 'restore_status': 'ready',
238 'restore_expires_at': result.get('expires_at'),
239 })
241 return {
242 'type': 'ir.actions.act_url',
243 'url': result['url'],
244 'target': 'new',
245 }
246 elif result.get('status') == 'pending':
247 return {
248 'type': 'ir.actions.client',
249 'tag': 'display_notification',
250 'params': {
251 'title': _('Not Ready'),
252 'message': _('Backup is still being prepared for download. Please try again later.'),
253 'type': 'warning',
254 'sticky': False,
255 }
256 }
257 else:
258 raise UserError(_('Download not available: %s') % result.get('error', 'Unknown error'))
259 except UserError:
260 raise
261 except Exception as e:
262 raise UserError(_('Failed to get download URL: %s') % str(e))
264 def action_cancel_backup(self):
265 """Cancel a pending or in-progress backup"""
266 self.ensure_one()
267 if self.status not in ('pending', 'in_progress', 'validating'):
268 raise UserError(_('Only pending or in-progress backups can be cancelled.'))
270 # TODO: Call API to cancel backup
271 self.write({
272 'status': 'cancelled',
273 })
275 return {
276 'type': 'ir.actions.client',
277 'tag': 'display_notification',
278 'params': {
279 'title': _('Backup Cancelled'),
280 'message': _('Backup has been cancelled.'),
281 'type': 'info',
282 'sticky': False,
283 }
284 }
286 @api.model
287 def cron_refresh_pending_backups(self):
288 """Cron job to refresh status of pending backups"""
289 pending_logs = self.search([
290 ('status', 'in', ('pending', 'in_progress', 'validating')),
291 ('backup_uuid', '!=', False),
292 ])
294 for log in pending_logs:
295 try:
296 log.action_refresh_status()
297 except Exception as e:
298 _logger.error(f'Failed to refresh backup {log.backup_uuid}: {e}')