Coverage for wizards / lox_backup_wizard.py: 16%
226 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
6import tempfile
7import os
8import json
10_logger = logging.getLogger(__name__)
13class LoxBackupWizard(models.TransientModel):
14 _name = 'lox.backup.wizard'
15 _description = 'LOX Backup Wizard'
17 config_id = fields.Many2one(
18 'lox.backup.config',
19 string='Configuration',
20 required=True,
21 default=lambda self: self._default_config_id(),
22 )
23 profile_id = fields.Many2one(
24 'lox.backup.profile',
25 string='Use Profile',
26 domain="[('config_id', '=', config_id), ('active', '=', True)]",
27 help='Select a saved profile or customize below',
28 )
30 # Backup type
31 backup_mode = fields.Selection([
32 ('full', 'Full Backup'),
33 ('custom', 'Custom Selection'),
34 ], string='Backup Mode', default='full', required=True)
36 # What to include
37 backup_database = fields.Boolean(
38 string='Database',
39 default=True,
40 help='Full PostgreSQL database dump',
41 )
42 backup_filestore = fields.Boolean(
43 string='Filestore',
44 default=True,
45 help='All attachments and uploaded files',
46 )
47 backup_modules = fields.Boolean(
48 string='Modules Info',
49 default=True,
50 help='List of installed modules with versions',
51 )
53 # Custom settings
54 custom_tags = fields.Char(
55 string='Additional Tags',
56 help='Comma-separated tags to add to this backup',
57 )
58 retention_days = fields.Integer(
59 string='Retention Days',
60 default=30,
61 help='Days to keep this backup (0 = use default)',
62 )
63 backup_name = fields.Char(
64 string='Backup Name',
65 help='Optional custom name for this backup',
66 )
67 description = fields.Text(
68 string='Description',
69 help='Optional description for this backup',
70 )
72 # Save as profile
73 save_as_profile = fields.Boolean(
74 string='Save as Profile',
75 default=False,
76 help='Save current settings as a reusable profile',
77 )
78 profile_name = fields.Char(
79 string='Profile Name',
80 )
82 @api.model
83 def _default_config_id(self):
84 config = self.env['lox.backup.config'].search([
85 ('active', '=', True),
86 ('source_registered', '=', True),
87 ], limit=1)
88 return config.id if config else False
90 @api.onchange('profile_id')
91 def _onchange_profile_id(self):
92 """Load profile settings"""
93 if self.profile_id:
94 self.backup_mode = 'custom'
95 self.backup_database = self.profile_id.include_database
96 self.backup_filestore = self.profile_id.include_filestore
97 self.backup_modules = self.profile_id.include_modules
98 self.custom_tags = self.profile_id.custom_tags
99 if self.profile_id.retention_days:
100 self.retention_days = self.profile_id.retention_days
102 @api.onchange('backup_mode')
103 def _onchange_backup_mode(self):
104 """Set defaults based on mode"""
105 if self.backup_mode == 'full':
106 self.backup_database = True
107 self.backup_filestore = True
108 self.backup_modules = True
110 @api.onchange('config_id')
111 def _onchange_config_id(self):
112 """Load config defaults"""
113 if self.config_id:
114 self.retention_days = self.config_id.retention_days
116 def _get_modules_info(self):
117 """Get installed modules information"""
118 modules = self.env['ir.module.module'].search([
119 ('state', '=', 'installed'),
120 ])
121 return [{
122 'name': m.name,
123 'version': m.installed_version or m.latest_version,
124 'author': m.author,
125 'summary': m.summary,
126 'category': m.category_id.name if m.category_id else '',
127 } for m in modules]
129 def _build_tags(self):
130 """Build tags list for the backup"""
131 tags = ['odoo']
133 # Add component tags
134 if self.backup_database:
135 tags.append('database')
136 if self.backup_filestore:
137 tags.append('filestore')
138 if self.backup_modules:
139 tags.append('modules')
141 # Add mode tag
142 if self.backup_mode == 'full':
143 tags.append('full')
144 else:
145 tags.append('custom')
147 # Add manual tag
148 tags.append('manual')
150 # Add config default tags
151 if self.config_id.default_tags:
152 for tag in self.config_id.default_tags.split(','):
153 tag = tag.strip()
154 if tag and tag not in tags:
155 tags.append(tag)
157 # Add custom tags
158 if self.custom_tags:
159 for tag in self.custom_tags.split(','):
160 tag = tag.strip()
161 if tag and tag not in tags:
162 tags.append(tag)
164 # Add profile tag if using profile
165 if self.profile_id:
166 tags.append(f'profile:{self.profile_id.name}')
168 return tags
170 def _save_profile(self):
171 """Save current settings as a profile"""
172 if self.save_as_profile and self.profile_name:
173 self.env['lox.backup.profile'].create({
174 'name': self.profile_name,
175 'config_id': self.config_id.id,
176 'include_database': self.backup_database,
177 'include_filestore': self.backup_filestore,
178 'include_modules': self.backup_modules,
179 'custom_tags': self.custom_tags,
180 'retention_days': self.retention_days if self.retention_days != self.config_id.retention_days else 0,
181 'description': self.description,
182 })
184 def action_run_backup(self):
185 """Execute manual backup"""
186 self.ensure_one()
188 config = self.config_id
189 if not config.source_registered:
190 raise UserError(_('Source is not registered. Please register first.'))
192 # Validate at least one component
193 if not any([self.backup_database, self.backup_filestore, self.backup_modules]):
194 raise UserError(_('Please select at least one component to backup.'))
196 api = self.env['lox.api'].create_client(config)
198 # Build components list
199 components = []
200 if self.backup_database:
201 components.append('database')
202 if self.backup_filestore:
203 components.append('filestore')
205 # Build tags
206 tags = self._build_tags()
208 # Get modules info if requested
209 modules_info = None
210 if self.backup_modules:
211 modules_info = self._get_modules_info()
213 # Determine component type for log
214 if self.backup_database and self.backup_filestore:
215 log_component = 'full'
216 elif self.backup_database:
217 log_component = 'database'
218 elif self.backup_filestore:
219 log_component = 'filestore'
220 else:
221 log_component = 'full' # modules only
223 try:
224 # Create backup log entry
225 log = self.env['lox.backup.log'].create({
226 'config_id': config.id,
227 'component': log_component,
228 'status': 'pending',
229 })
231 # Build metadata
232 metadata = {
233 'manual_backup': True,
234 'odoo_database': self.env.cr.dbname,
235 'user': self.env.user.name,
236 'backup_mode': self.backup_mode,
237 'include_database': self.backup_database,
238 'include_filestore': self.backup_filestore,
239 'include_modules': self.backup_modules,
240 }
242 if modules_info:
243 metadata['modules'] = modules_info
244 metadata['modules_count'] = len(modules_info)
246 if self.profile_id:
247 metadata['profile_id'] = self.profile_id.id
248 metadata['profile_name'] = self.profile_id.name
250 if self.backup_name:
251 metadata['custom_name'] = self.backup_name
253 if self.description:
254 metadata['description'] = self.description
256 # Create backup via API
257 backup_data = {
258 'source_id': config.source_id,
259 'components': components if components else ['database'],
260 'tags': ','.join(tags),
261 'retention_days': self.retention_days or config.retention_days,
262 'metadata': metadata,
263 }
265 if self.backup_name:
266 backup_data['name'] = self.backup_name
268 result = api.create_backup(backup_data)
270 if result.get('id'):
271 log.write({
272 'backup_uuid': result['id'],
273 'status': 'in_progress',
274 'started_at': fields.Datetime.now(),
275 })
277 # Save profile if requested
278 self._save_profile()
280 # Update profile stats if using one
281 if self.profile_id:
282 self.profile_id._update_stats()
284 return {
285 'type': 'ir.actions.client',
286 'tag': 'display_notification',
287 'params': {
288 'title': _('Backup Started'),
289 'message': _('Backup job created with UUID: %s') % result['id'],
290 'type': 'success',
291 'sticky': False,
292 'next': {
293 'type': 'ir.actions.act_window',
294 'res_model': 'lox.backup.log',
295 'res_id': log.id,
296 'view_mode': 'form',
297 'target': 'current',
298 }
299 }
300 }
301 else:
302 log.write({
303 'status': 'failed',
304 'error_message': result.get('error', 'Unknown error'),
305 })
306 raise UserError(_('Backup failed: %s') % result.get('error', 'Unknown error'))
308 except UserError:
309 raise
310 except Exception as e:
311 _logger.exception('Manual backup failed')
312 if 'log' in locals():
313 log.write({
314 'status': 'failed',
315 'error_message': str(e),
316 })
317 raise UserError(_('Backup failed: %s') % str(e))
319 def action_run_backup_with_upload(self):
320 """Execute backup with local file creation and upload"""
321 self.ensure_one()
323 config = self.config_id
324 if not config.source_registered:
325 raise UserError(_('Source is not registered. Please register first.'))
327 # Validate at least one component
328 if not any([self.backup_database, self.backup_filestore, self.backup_modules]):
329 raise UserError(_('Please select at least one component to backup.'))
331 from ..models.lox_api import OdooBackupCreator
333 api = self.env['lox.api'].create_client(config)
334 backup_creator = OdooBackupCreator(self.env)
336 # Build tags
337 tags = self._build_tags()
339 # Get modules info if requested
340 modules_info = None
341 if self.backup_modules:
342 modules_info = self._get_modules_info()
344 # Determine component type for log
345 if self.backup_database and self.backup_filestore:
346 log_component = 'full'
347 elif self.backup_database:
348 log_component = 'database'
349 elif self.backup_filestore:
350 log_component = 'filestore'
351 else:
352 log_component = 'full'
354 try:
355 # Create backup log entry
356 log = self.env['lox.backup.log'].create({
357 'config_id': config.id,
358 'component': log_component,
359 'status': 'pending',
360 'started_at': fields.Datetime.now(),
361 })
363 # Create local backup based on selections
364 backup_file = None
365 if self.backup_database and self.backup_filestore:
366 backup_file = backup_creator.create_full_backup()
367 elif self.backup_database:
368 backup_file = backup_creator.create_database_backup()
369 elif self.backup_filestore:
370 backup_file = backup_creator.create_filestore_backup()
371 else:
372 # Modules only - create a JSON file
373 backup_file = self._create_modules_only_backup(modules_info)
375 if not backup_file:
376 raise UserError(_('Failed to create local backup.'))
378 # If we have modules info and backup_file is a tarball, inject it
379 if modules_info and self.backup_database:
380 self._inject_modules_info(backup_file, modules_info)
382 # Get file size
383 file_size = os.path.getsize(backup_file)
384 log.write({'size_bytes': file_size})
386 # Build metadata
387 metadata = {
388 'manual_backup': True,
389 'with_upload': True,
390 'odoo_database': self.env.cr.dbname,
391 'user': self.env.user.name,
392 'backup_mode': self.backup_mode,
393 'include_database': self.backup_database,
394 'include_filestore': self.backup_filestore,
395 'include_modules': self.backup_modules,
396 }
398 if modules_info:
399 metadata['modules_count'] = len(modules_info)
401 if self.profile_id:
402 metadata['profile_id'] = self.profile_id.id
403 metadata['profile_name'] = self.profile_id.name
405 # Build components list
406 components = []
407 if self.backup_database:
408 components.append('database')
409 if self.backup_filestore:
410 components.append('filestore')
411 if not components:
412 components = ['modules']
414 # Build upload options for single-step upload
415 upload_options = {
416 'tags': tags,
417 'retention_days': self.retention_days or config.retention_days,
418 'component': ','.join(components),
419 'metadata': metadata,
420 'description': f'Odoo backup from {self.env.cr.dbname}',
421 }
423 if self.backup_name:
424 upload_options['name'] = self.backup_name
426 # Upload backup (single-step: creates record and uploads file)
427 log.write({'status': 'in_progress'})
428 upload_result = api.upload_backup(backup_file, upload_options)
430 # Check for upload errors
431 if upload_result.get('error') or upload_result.get('success') is False:
432 raise UserError(_('Upload failed: %s') % upload_result.get('error', 'Unknown'))
434 # Get backup UUID from response
435 backup_uuid = upload_result.get('id') or upload_result.get('uuid')
436 if backup_uuid:
437 log.write({'backup_uuid': backup_uuid})
439 # Cleanup temp file
440 try:
441 os.remove(backup_file)
442 temp_dir = os.path.dirname(backup_file)
443 if not os.listdir(temp_dir):
444 os.rmdir(temp_dir)
445 except:
446 pass
448 # Upload succeeded (errors already handled above)
449 log.write({'status': 'validating'})
451 # Save profile if requested
452 self._save_profile()
454 # Update profile stats if using one
455 if self.profile_id:
456 self.profile_id._update_stats()
458 return {
459 'type': 'ir.actions.client',
460 'tag': 'display_notification',
461 'params': {
462 'title': _('Backup Uploaded'),
463 'message': _('Backup uploaded successfully. UUID: %s') % backup_uuid,
464 'type': 'success',
465 'sticky': False,
466 'next': {
467 'type': 'ir.actions.act_window',
468 'res_model': 'lox.backup.log',
469 'res_id': log.id,
470 'view_mode': 'form',
471 'target': 'current',
472 }
473 }
474 }
476 except UserError:
477 raise
478 except Exception as e:
479 _logger.exception('Backup with upload failed')
480 if 'log' in locals():
481 log.write({
482 'status': 'failed',
483 'error_message': str(e),
484 })
485 raise UserError(_('Backup failed: %s') % str(e))
487 def _create_modules_only_backup(self, modules_info):
488 """Create a backup containing only modules information"""
489 import tarfile
490 import gzip
492 temp_dir = tempfile.mkdtemp()
493 modules_file = os.path.join(temp_dir, 'modules.json')
494 backup_file = os.path.join(temp_dir, f'{self.env.cr.dbname}_modules.tar.gz')
496 # Write modules info
497 with open(modules_file, 'w') as f:
498 json.dump({
499 'database': self.env.cr.dbname,
500 'timestamp': fields.Datetime.now().isoformat(),
501 'modules': modules_info,
502 }, f, indent=2)
504 # Create tarball
505 with tarfile.open(backup_file, 'w:gz') as tar:
506 tar.add(modules_file, arcname='modules.json')
508 os.remove(modules_file)
509 return backup_file
511 def _inject_modules_info(self, backup_file, modules_info):
512 """Inject modules.json into existing backup archive"""
513 # For now, we'll include it in metadata
514 # In a full implementation, we would repack the tarball
515 pass