first commit
This commit is contained in:
2
ct_client_backup/__init__.py
Executable file
2
ct_client_backup/__init__.py
Executable file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizards
|
||||
31
ct_client_backup/__manifest__.py
Executable file
31
ct_client_backup/__manifest__.py
Executable file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
{
|
||||
'name': "Clients Periodic Backups | SaaS",
|
||||
|
||||
'summary': """
|
||||
Take Periodic Backups of Client Instances""",
|
||||
|
||||
'description': """
|
||||
Take Periodic Backups of Client Instances""",
|
||||
|
||||
'author': "Muhammad Awais",
|
||||
'website': "https://codetuple.io",
|
||||
'category': 'Uncategorized',
|
||||
'version': '2.0.0',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
'depends': ['base', 'kk_odoo_saas','queue_job'],
|
||||
|
||||
# always loaded
|
||||
'data': [
|
||||
# "security/security.xml",
|
||||
'security/ir.model.access.csv',
|
||||
'wizards/backup_restore.xml',
|
||||
'views/views.xml',
|
||||
'views/app_views.xml',
|
||||
'data/backup_ignite_cron.xml',
|
||||
],
|
||||
"application": True,
|
||||
|
||||
}
|
||||
0
ct_client_backup/controllers/__init__.py
Executable file
0
ct_client_backup/controllers/__init__.py
Executable file
14
ct_client_backup/data/backup_ignite_cron.xml
Executable file
14
ct_client_backup/data/backup_ignite_cron.xml
Executable file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="backup_process_ignite_crone" model="ir.cron">
|
||||
<field name="name">SaaS: Backup Process Ignite Cron</field>
|
||||
<field name="model_id" ref="model_kk_odoo_saas_app"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.ignite_backup_server_cron()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
2
ct_client_backup/models/__init__.py
Normal file
2
ct_client_backup/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import saas_app
|
||||
from . import models
|
||||
137
ct_client_backup/models/models.py
Normal file
137
ct_client_backup/models/models.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError, MissingError
|
||||
import requests
|
||||
import xmlrpc
|
||||
import logging
|
||||
import base64
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SaaSAppBackup(models.Model):
|
||||
_name = 'kk_odoo_saas.app.backup'
|
||||
_description = 'SaaS App Backup'
|
||||
|
||||
name = fields.Char()
|
||||
app = fields.Many2one('kk_odoo_saas.app', 'SaaS App')
|
||||
file_name = fields.Char(string="File Name")
|
||||
file_path = fields.Char(string="File Path")
|
||||
url = fields.Char(string="Url")
|
||||
backup_date_time = fields.Datetime(string="Backup Time (UTC)")
|
||||
status = fields.Selection(string="Status", selection=[('failed', 'Failed'), ('success', 'Success')])
|
||||
message = fields.Char(string="Message")
|
||||
file_size = fields.Char(string="File Size")
|
||||
|
||||
def download_db_file(self):
|
||||
"""
|
||||
to download the database backup, it stores the file in attachment
|
||||
:return: Action
|
||||
"""
|
||||
file_path = self.file_path
|
||||
_logger.info("------------ %r ----------------" % file_path)
|
||||
if self.url:
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': self.url,
|
||||
'target': 'new',
|
||||
}
|
||||
try:
|
||||
with open(file_path, 'rb') as reader:
|
||||
result = base64.b64encode(reader.read())
|
||||
except IOError as e:
|
||||
raise MissingError('Unable to find File on the path')
|
||||
attachment_obj = self.env['ir.attachment'].sudo()
|
||||
name = self.file_name
|
||||
attachment_id = attachment_obj.create({
|
||||
'name': name,
|
||||
'datas': result,
|
||||
'public': False
|
||||
})
|
||||
download_url = '/web/content/' + str(attachment_id.id) + '?download=true'
|
||||
_logger.info("--- %r ----" % download_url)
|
||||
self.url = download_url
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': download_url,
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_restore_backup_to_instance(self, restore_to_id=False):
|
||||
"""
|
||||
it will restore the backup to a new instance,
|
||||
the instance should be created manually,
|
||||
and there should be no database at new_instance.com/web/database/selector
|
||||
:param: restore_to_id is kk_saa_app object on which we have to restore backup
|
||||
:return: False
|
||||
"""
|
||||
if self.app and restore_to_id:
|
||||
restore_url = restore_to_id.get_url()
|
||||
if self.file_path and os.path.exists(self.file_path) and requests.get(restore_url).status_code < 400:
|
||||
db_list = []
|
||||
try:
|
||||
db_list = xmlrpc.client.ServerProxy(restore_url + '/xmlrpc/db').list()
|
||||
except xmlrpc.client.ProtocolError as e:
|
||||
_logger.info("There is no database on Db selector")
|
||||
|
||||
_logger.info("All Databases on Postgres Server -> {} <-".format(db_list))
|
||||
_logger.info("New Db name: {}".format(restore_to_id.app_name))
|
||||
if restore_to_id.app_name not in db_list:
|
||||
self.restore_backup_to_client(self.file_path, restore_url, restore_to_id.app_name,
|
||||
restore_to_id.backup_master_pass)
|
||||
else:
|
||||
raise UserError("Cant restore Backup, Database already existed, please delete it.")
|
||||
else:
|
||||
raise UserError("Cant restore Backup! the url is not accessible or backup file not exists.")
|
||||
else:
|
||||
_logger.error("Cant restore Backup, Backup Id, or Restore App Missing")
|
||||
raise UserError("Cant restore Backup, Backup Id, or Restore App Missing")
|
||||
|
||||
def restore_backup_to_client(self, file_path, restore_url, db_name, master_pwd):
|
||||
if file_path and restore_url and db_name and master_pwd:
|
||||
restore_url = restore_url + '/web/database/restore'
|
||||
data = {
|
||||
'master_pwd': master_pwd,
|
||||
'name': db_name,
|
||||
'copy': 'true',
|
||||
'backup_file': '@' + file_path
|
||||
}
|
||||
backup = open(file_path, "rb")
|
||||
try:
|
||||
response = requests.post(restore_url, data=data, files={"backup_file": backup})
|
||||
if response.status_code == 200:
|
||||
_logger.info("Restore Done, this is the response Code: {}".format(response.status_code))
|
||||
else:
|
||||
_logger.info("Restore Done, this is the response Code: {}".format(response.status_code))
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'success': False,
|
||||
}
|
||||
else:
|
||||
_logger.error("Cant restore Db One of the parameter is Missing")
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('saas_app.backup')
|
||||
res = super(SaaSAppBackup, self).create(vals)
|
||||
return res
|
||||
|
||||
def calc_backup_size(self):
|
||||
if not os.path.exists(self.file_path):
|
||||
return
|
||||
# calculate file size in KB, MB, GB
|
||||
|
||||
def convert_bytes(size):
|
||||
""" Convert bytes to KB, or MB or GB"""
|
||||
for x in ['bytes', 'KB', 'MB', 'GB', 'TB']:
|
||||
if size < 1024.0:
|
||||
return "%3.1f %s" % (size, x)
|
||||
size /= 1024.0
|
||||
|
||||
self.file_size = convert_bytes(os.path.getsize(self.file_path))
|
||||
124
ct_client_backup/models/saas_app.py
Executable file
124
ct_client_backup/models/saas_app.py
Executable file
@@ -0,0 +1,124 @@
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
from odoo import models, fields, api, _
|
||||
import logging
|
||||
import os
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SaaSApp(models.Model):
|
||||
_inherit = 'kk_odoo_saas.app'
|
||||
backup_db_name = fields.Char(string="Database Name", default=lambda a: a.client_db_name)
|
||||
backup_master_pass = fields.Char(string="Master Password")
|
||||
backups_enabled = fields.Boolean()
|
||||
|
||||
backups = fields.Many2many(comodel_name='kk_odoo_saas.app.backup', string='Backups')
|
||||
|
||||
def action_create_backup(self):
|
||||
"""
|
||||
It is being called from 2 locations
|
||||
:return:
|
||||
"""
|
||||
for app in self:
|
||||
response = self.backup_db()
|
||||
backup = self.env['kk_odoo_saas.app.backup'].create({'backup_date_time': fields.Datetime.now(),
|
||||
'app': app.id,
|
||||
'file_name': response.get('filename'),
|
||||
'file_path': response.get('filepath'),
|
||||
'message': response.get('message')
|
||||
})
|
||||
if response.get('success'):
|
||||
backup.write({'status': 'success'})
|
||||
else:
|
||||
backup.write({'status': 'failed'})
|
||||
app.write({'backups': [(4, backup.id)]})
|
||||
|
||||
def action_delete_old_backup(self):
|
||||
for app in self:
|
||||
for backup in app.backups:
|
||||
if backup.backup_date_time < fields.Datetime.now() - timedelta(days=7.0):
|
||||
if os.path.exists(backup.file_path):
|
||||
try:
|
||||
os.remove(backup.file_path)
|
||||
if backup.url:
|
||||
# deleting the attachments related to this backup
|
||||
att_id = backup.url.replace('?download=true', '').replace('/web/content/', '')
|
||||
if att_id:
|
||||
try:
|
||||
attch_id = int(att_id)
|
||||
if attch_id:
|
||||
self.env['ir.attachment'].browse([attch_id]).unlink()
|
||||
except ValueError as e:
|
||||
_logger.error(e)
|
||||
backup.unlink()
|
||||
except OSError as e:
|
||||
_logger.error("Error while deleting file: %s - %s." % (e.filename, e.strerror))
|
||||
else:
|
||||
_logger.error("The file does not exist")
|
||||
|
||||
def backup_db(self):
|
||||
"""
|
||||
Actual Backup function
|
||||
:return:
|
||||
"""
|
||||
# get the creds for db manager
|
||||
data = {
|
||||
'master_pwd': self.backup_master_pass,
|
||||
'name': self.backup_db_name,
|
||||
'backup_format': 'zip'
|
||||
}
|
||||
|
||||
client_url = 'https://{0}{1}'.format(self.sub_domain_name, self.domain_name)
|
||||
msg = ''
|
||||
|
||||
# where we want to store backups, in the linux user, with which the odoo-service is running
|
||||
backup_dir = os.path.join(os.path.expanduser('~'), 'client_backups')
|
||||
if not os.path.exists(backup_dir):
|
||||
os.mkdir(backup_dir)
|
||||
|
||||
backup_dir = os.path.join(backup_dir, self.sub_domain_name)
|
||||
if not os.path.exists(backup_dir):
|
||||
os.mkdir(backup_dir)
|
||||
|
||||
client_url += '/web/database/backup'
|
||||
# Without Streaming method
|
||||
# response = requests.post(client_url, data=data)
|
||||
# Streaming zip, so that everything is not stored in RAM.
|
||||
|
||||
try:
|
||||
filename = self.backup_db_name + '-' + fields.Datetime.now().strftime("%m-%d-%Y-%H-%M") + '.zip'
|
||||
backed_up_file_path = os.path.join(backup_dir, filename)
|
||||
with requests.post(client_url, data=data, stream=True) as response:
|
||||
response.raise_for_status()
|
||||
with open(os.path.join(backup_dir, filename), 'wb') as file:
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
file.write(chunk)
|
||||
msg = 'Database backup Successful at ' + fields.Datetime.now().strftime("%m-%d-%Y-%H:%M:%S")
|
||||
return {
|
||||
'success': True,
|
||||
'msg': msg,
|
||||
'filename': filename,
|
||||
'filepath': backed_up_file_path
|
||||
}
|
||||
except Exception as e:
|
||||
msg = 'Failed at ' + fields.Datetime.now().strftime("%m-%d-%Y-%H:%M:%S") + ' ' + str(e)
|
||||
return {
|
||||
'success': False,
|
||||
'msg': msg
|
||||
}
|
||||
|
||||
@api.model
|
||||
def ignite_backup_server_cron(self):
|
||||
"""
|
||||
A Scheduled Action which will take new backups and del old
|
||||
:return: False
|
||||
"""
|
||||
# search for saas instance in lanched and modified states and backups enabled
|
||||
apps = self.env['kk_odoo_saas.app'].sudo().search(
|
||||
[('status', 'in', ['l', 'm']), ('backups_enabled', '=', True)])
|
||||
|
||||
for app in apps:
|
||||
app.action_create_backup()
|
||||
app.action_delete_old_backup()
|
||||
3
ct_client_backup/security/ir.model.access.csv
Executable file
3
ct_client_backup/security/ir.model.access.csv
Executable file
@@ -0,0 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_saas_app_backups,access_saas_app_backups,model_kk_odoo_saas_app_backup,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
access_saas_app_backup_restore_wizard,SaaS Manager Backup Restore Permissions,model_saas_client_backup_restore_wizard,kk_odoo_saas.group_saas_manager,1,1,1,1
|
||||
|
BIN
ct_client_backup/static/description/icon.png
Executable file
BIN
ct_client_backup/static/description/icon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
37
ct_client_backup/views/app_views.xml
Executable file
37
ct_client_backup/views/app_views.xml
Executable file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Inherit Form View to Modify it -->
|
||||
<record id="saas_app_form_backup_inherit" model="ir.ui.view">
|
||||
<field name="name">SaaS App inherit for Backups</field>
|
||||
<field name="model">kk_odoo_saas.app</field>
|
||||
<field name="inherit_id" ref="kk_odoo_saas.kk_odoo_saas_app_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='k8s_logs']" position="after">
|
||||
<page name="backups" string="Backups">
|
||||
<group>
|
||||
<group>
|
||||
<field name="backups_enabled" widget="boolean_toggle"/>
|
||||
<field name="backup_db_name"
|
||||
attrs="{'required': [('backups_enabled', '=', True)], 'invisible': [('backups_enabled', '!=', True)]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="backup_master_pass"
|
||||
attrs="{'required': [('backups_enabled', '=', True)], 'invisible': [('backups_enabled', '!=', True)]}"
|
||||
password="1"/>
|
||||
<button name="action_create_backup"
|
||||
type="object"
|
||||
string="Take Backup"
|
||||
attrs="{'invisible': ['|', ('status','in',('d','del')),('backups_enabled', '=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="backups" nolabel="1" readonly="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
75
ct_client_backup/views/views.xml
Executable file
75
ct_client_backup/views/views.xml
Executable file
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="kk_odoo_saas_app_backup_form_view" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas.kk_odoo_saas.app.backup.form</field>
|
||||
<field name="model">kk_odoo_saas.app.backup</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="download_db_file"
|
||||
type="object"
|
||||
string="Download File"
|
||||
/>
|
||||
<button name="action_restore_backup_to_instance"
|
||||
type="object"
|
||||
string="Restore to Instance"
|
||||
/>
|
||||
|
||||
</header>
|
||||
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="Title" readonly="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="app"/>
|
||||
<field name="file_name"/>
|
||||
<field name="file_path"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="url"/>
|
||||
<field name="backup_date_time"/>
|
||||
<field name="status"/>
|
||||
<field name="message"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kk_odoo_saas_app_backup_tree_view" model="ir.ui.view">
|
||||
<field name="name">kk_odoo_saas.kk_odoo_saas.app.backup.tree</field>
|
||||
<field name="model">kk_odoo_saas.app.backup</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="App Backup" decoration-success="status=='success'" decoration-danger="status!='success'">
|
||||
<field name="name" readonly="1"/>
|
||||
<field name="file_name"/>
|
||||
<field name="backup_date_time"/>
|
||||
<field name="status"/>
|
||||
<field name="file_size"/>
|
||||
<button name="download_db_file"
|
||||
type="object" icon="fa-cloud-download"
|
||||
string="Download Zip File"
|
||||
class="btn-secondary"
|
||||
/>
|
||||
<button name="ct_client_backup.action_saas_client_backup_restore_wizard"
|
||||
string="Restore to Instance" type="action" icon="fa-cloud-upload"
|
||||
class="btn-secondary"
|
||||
context="{'default_backup_id':id}"
|
||||
attrs="{'invisible':[('status','=', 'failed')]}" />
|
||||
|
||||
<button name="calc_backup_size" type="object"
|
||||
icon="fa-refresh" class="btn-secondary"
|
||||
string="Calculate file Size"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
1
ct_client_backup/wizards/__init__.py
Executable file
1
ct_client_backup/wizards/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
from . import backup_restore
|
||||
23
ct_client_backup/wizards/backup_restore.py
Executable file
23
ct_client_backup/wizards/backup_restore.py
Executable file
@@ -0,0 +1,23 @@
|
||||
from odoo import fields, models
|
||||
import logging
|
||||
from odoo.exceptions import UserError, MissingError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BackupRestore(models.TransientModel):
|
||||
_name = 'saas.client.backup.restore.wizard'
|
||||
name = fields.Char('Name')
|
||||
backup_id = fields.Many2one('kk_odoo_saas.app.backup', 'Backup Name')
|
||||
restore_to = fields.Many2one('kk_odoo_saas.app', 'Restore Backup To')
|
||||
|
||||
def action_call_restore_function(self):
|
||||
"""
|
||||
It will call the Backup Function Async, Thanks to queue_job module
|
||||
:return:
|
||||
"""
|
||||
if self.backup_id and self.backup_id.app and self.restore_to:
|
||||
self.backup_id.action_restore_backup_to_instance(self.restore_to)
|
||||
else:
|
||||
_logger.error("Cant restore Backup, Backup Id, or Restore App Missing")
|
||||
raise UserError("Cant restore Backup, Backup Id, or Restore App Missing")
|
||||
45
ct_client_backup/wizards/backup_restore.xml
Executable file
45
ct_client_backup/wizards/backup_restore.xml
Executable file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="saas_client_backup_restore_view_form" model="ir.ui.view">
|
||||
<field name="name">saas_client_backup_restore_view_form</field>
|
||||
<field name="model">saas.client.backup.restore.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="backup_id" required="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="restore_to" string="Restore to SaaS Instance" required="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button
|
||||
name="action_call_restore_function"
|
||||
string="Start Restoring Process"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button string="Cancel" class="oe_link" special="cancel"/>
|
||||
</footer>
|
||||
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_saas_client_backup_restore_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Restore the Backup to SaaS App</field>
|
||||
<field name="res_model">saas.client.backup.restore.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="saas_client_backup_restore_view_form"/>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="ct_client_backup.model_kk_odoo_saas_app_backup"/>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user