diff --git a/wk_backup_restore/LICENSE b/wk_backup_restore/LICENSE new file mode 100644 index 0000000..b534039 --- /dev/null +++ b/wk_backup_restore/LICENSE @@ -0,0 +1,398 @@ +SOFTWARE LICENCE AGREEMENT +========================== + +This AGREEMENT is made effective on the date of the purchase of the software +between Webkul Software Pvt. Ltd.,Company incorporated under the Companies +Act, 1956 (hereinafter referred to as “Licensor"), and the purchaser of the +software/ product (hereinafter referred to as "Licensee"). + + +Preamble +-------- + +Licensor is a web and mobile product based organization engaged in the +business of developing and marketing software for enterprise level e-commerce +businesses. It is an ISO and NSR (NASSCOM) certified organization having a +team of more than 150 creative engineers which come from different +backgrounds. It has developed more than 700 web extensions and apps in the +past few years for open source platforms which are used and trusted globally. +Licensee now wishes to obtain license, and Licensor wishes to grant a license, +to allow use of the software so purchased in developing the e-commerce +business website/ mobile app of the Licensee, subject to the terms and +conditions set forth herein. + +THEREFORE, with the intent to be legally bound, the parties hereby agree as +follows: + + +Agreement +--------- + +1.DEFINITIONS. +As used in this Agreement, the following capitalized terms +shall have the definitions set forth below: + +"Derivative Works" are works developed by Licensee, its officers, agents, +contractors or employees, which are based upon, in whole or in part, the +Source Code and/or the Documentation and may also be based upon and/or +incorporate one or more other preexisting works of the Licensor. Derivative +Works may be any improvement, revision, modification, translation (including +compilation or recapitulation by computer), abridgment, condensation, +expansion, or any other form in which such a preexisting work may be recast, +transformed, or adapted. For purposes hereof, a Derivative Work shall also +include any compilation that incorporates such a preexisting work. + +"Documentation" is written, printed or otherwise recorded or stored (digital +or paper) material relating to the Software and/or Source Code, including +technical specifications and instructions for its use including Software/ +Source Code annotations and other descriptions of the principles of its +operation and instructions for its use. + +"Improvements" shall mean, with respect to the Software, all modifications and +changes made, developed, acquired or conceived after the date hereof and +during the entire term of this Agreement. + +"Source Code" is the computer programming source code form of the Software in +the form maintained by the Licensor, and includes all non-third-party +executables, libraries, components, and Documentation created or used in the +creation, development, maintenance, and support of the Software as well as all +updates, error corrections and revisions thereto provided by Licensor, in +whole or in part. + + +2.SOFTWARE LICENSE. + +(a)Grant of License. For the consideration set forth below, Licensor hereby +grants to Licensee, and Licensee hereby accepts the worldwide, non-exclusive, +perpetual, royalty-free rights and licenses set forth below: + +(i)The right and license to use and incorporate the software, in whole or in +part, to develop its website/ mobile app (including the integration of all or +part of the Licensor’s software into Licensee's own software) on one domain ( +Except Joomla modules , listed on store are entitled to be used on unlimited +domain as per the standard guidelines ) only, solely for the own personal or +business use of the Licensee. However, the License does not authorize the +Licensee to compile, copy or distribute the said Software or its Derivative +Works. + +(ii)The right and license does not authorize the Licensee to share any backup +or archival copies of the Software and / or the Source Code and Documentation +on any public internet space including github , stackoverflow etc . The +Licensee must ensure that the backup are not accessible to any other person +and the Licensee must prevent copying / use of source code by any unauthorized +persons. + +(iii)The right and license does not authorize the Licensee to migrate the +domain license to another domain. + +(iv)Our Joomla extensions are published under the GNU/GPL. + + +(b)Scope; Rights and Responsibilities. + +(i)Licensor shall enable the Licensee to download one complete copy of the +Software. + +(ii)The Software is intended for the sole use of the Licensee in development +of its own website/ mobile app. + +(iii)Licensee does not have the right to hand over, sell, distribute, +sub-license, rent, lease or lend any portion of the Software or Documentation, +whether modified or unmodified, to anyone. Licensee should not place the +Software on a server so that it becomes accessible via a public network such +as the Internet for distribution purposes. In case the Licensee is using any +source code management system like github, it can use the code there only when +it has paid subscription from such management system. + +(iv) In case the Licensee purchases the module and allow the third party +development agency to customize as per its need, it is at liberty to do so +subject to the condition that the Licensee as well as the Agency are not +authorized to sell the modified version of the extension. Except for the +required customization purposes, Licensee is not authorized to release the +Source Code, Derivative Work source code and/or Documentation to any third +party, which shall be considered as violation of the Agreement, inter-alia +entailing forthwith termination and legal action. + + +(c)Ownership. + +(i)Software and Source Code. All right, title, copyright, and interest in the +Software, Source Code, Software Modifications and Error corrections will be +and remain the property of Licensor. + +(ii)Derivative Works. As creation of Derivative Works by the Licensee is +prohibited, thus, all right, title, copyright, and interest in any and/or all +Derivative Works and Improvements created by, or on behalf of, Licensee will +also be deemed to the property of Licensor. Licensor shall be entitled to +protect copyright / intellectual property in all such Derivative Works and +Improvements also in any country as it may deem fit including without +limitation seeking copyright and/or patent protection. + + +3.CONSIDERATION. + +(a)Licensee shall pay to Licensor the amount as mentioned on the website from +where the order is placed, as one-time, upfront fees in consideration for the +licenses and rights granted hereunder (hereinafter referred to as the "License +Fee"). The License Fee to be paid by Licensee shall be paid upfront at the +time of placing the order, and no credit will be allowed under any +circumstances. + +(b)Once paid, the License Fees shall be non-refundable. The Licensee has fully +satisfied itself about the Software and has seen the demonstration, and only +thereafter has placed the order. Thus, the License Fees or any part thereof is +non-refundable. No claim for refund of the Licence Fees shall be entertained +under any circumstances. + + +4.REPRESENTATIONS AND WARRANTIES. + +(a)Mutual. Each of the parties represents and warrants to the other as +follows. + +(i)such party is a legal entity duly organized, validly existing and in good +standing; + +(ii)such party has the power and authority to conduct its business as +presently conducted and to enter into, execute, deliver and perform this +Agreement. + +(iii)This Agreement has been duly and validly accepted by such party and +constitutes the legal, valid and binding obligations of such party +respectively, enforceable against such party in accordance with their +respective terms; + +(iv)the acceptance, execution, delivery and performance of this Agreement does +not and will not violate such party's charter or by-laws; nor require any +consent, authorization, approval, exemption or other action by any third party +or governmental entity. + + +(b)Licensor warrants that, at the time of purchase of the Software: + +the Software will function materially as set forth in the website or published +functionality provided by Licensor to customers and potential customers +describing the Software; and + +Software add-ons, if purchased by the Licensee from the Licensor, will not +materially diminish the features or functions of or the specifications of the +Software as they existed as of the execution of this Agreement. + + +(c)Title. Licensor represents and warrants that it is the exclusive owner of +all copyright/ intellectual property in the Software (including the Source +Code) and has good and marketable title to the Software (including the Source +Code) free and clear of all liens, claims and encumbrances of any nature +whatsoever (collectively, "Liens"). Licensor's grant of license and rights to +Licensee hereunder does not, and will not infringe any third party's property, +intellectual property or personal rights. + + +5.TERM. + +(a)Subject to Licensee's payment obligations, this Agreement shall commence as +on the date of making payment of the Software by the Licensee to the Licensor, +and shall continue until terminated by either party. + +(b)The Licensor retains the right to terminate the license at any time, if the +Licensee is not abiding by any of the terms of the Agreement. The Licensee may +terminate the Agreement at any time at its own discretion by uninstalling the +Software and /or by destroying the said Software (or any copies thereof). +However, the Licensee shall not be entitled to seek any refund of the amount +paid by it to the Licensor, under any circumstances. + +(c)Survival. In the event this Agreement is terminated for any reason, the +provisions set forth in Sections 2(a), 2(b), and 2(c) shall survive. + + +6.INDEMNIFICATION. + +The Licensee release the Licensor from, and agree to indemnify, defend and +hold harmless the Licensor (and its officers, directors, employees, agents and +Affiliates) against, any claim, loss, damage, settlement, cost, taxes, expense +or other liability (including, without limitation, attorneys' fees) (each, a +"Claim") arising from or related to: (a) any actual or alleged breach of any +obligations in this Agreement; (b) any refund, adjustment, or return of +Software,(c) any claim for actual or alleged infringement of any Intellectual +Property Rights made by any third party or damages related thereto; or (d) +Taxes. + + +7.LIMITATION OF LIABILITY. + +The Licensor will not be liable for any direct, indirect, incidental, special, +consequential or exemplary damages, including but not limited to, damages for +loss of profits, goodwill, use, data or other intangible losses arising out of +or in connection with the Software, whether in contract, warranty, tort etc. ( +including negligence, software liability, any type of civil responsibility or +other theory or otherwise) to the Licensee or any other person for cost of +software, cover, recovery or recoupment of any investment made by the Licensee +or its affiliates in connection with this Agreement, or for any other loss of +profit, revenue, business, or data or punitive or consequential damages +arising out of or relating to this Agreement. Further, the aggregate liability +of the Licensor, arising out of or in connection with this Agreement or the +transactions contemplated hereby will not exceed at any time, or under any +circumstances, the total amounts received by the Licensor from the Licensee in +connection with the particular software giving rise to the claim. + + +8.FORCE MAJEURE. + +The Licensor will not be liable for any delay or failure to perform any of its +obligations under this Agreement by reasons, events or other matters beyond +its reasonable control. + + +9.RELATIONSHIP OF PARTIES. + +The Licensor and Licensee are independent legal entities, and nothing in this +Agreement will be construed to create a partnership, joint venture, +association of persons, agency, franchise, sales representative, or employment +relationship between the parties. The Licensee will have no authority to make +or accept any offers or representations on behalf of the Licensor. The +relationship between the parties is that of Licensor and Licensee only, and +the rights, duties, liabilities of each party shall be governed by this +Agreement. + + +10.MODIFICATION. + +The Licensor may amend any of the terms and conditions contained in this +Agreement at any time and solely at its discretion. Any changes will be +effective upon the posting of such changes on the Portal/ website, and the +Licensee is responsible for reviewing these changes and informing itself of +all applicable changes or notices. The continued use of a software by the +Licensee after posting of any changes by the Licensor, will constitute the +acceptance of such changes or modifications by the Licensee. + + +11.MISCELLANEOUS. + +(a)General Provisions. This Agreement: (i) may be amended only by a writing +signed by each of the parties; (ii) may be executed in several counterparts, +each of which shall be deemed an original but all of which shall constitute +one and the same instrument; (iii) contains the entire agreement of the +parties with respect to the transactions contemplated hereby and supersedes +all prior written and oral agreements, and all contemporaneous oral +agreements, relating to such transactions; (iv) shall be governed by, and +construed and enforced in accordance with, the laws of India; and (v) shall be +binding upon, and inure to the benefit of, the parties and their respective +successors and permitted assigns. Each of the parties hereby irrevocably +submits to the jurisdiction of the Courts at Delhi, India, for the purposes of +any action or proceeding arising out of or relating to this Agreement or the +subject matter hereof and brought by any other party. + +(b)Assignment. Except for the purpose of customization as mentioned in clause +2(b)(iv) above, Licensee cannot assign, pledge or otherwise transfer, whether +by operation of law or otherwise, this Agreement, or any of its obligations +hereunder, without the prior written consent of Licensor, which consent shall +not be unreasonably withheld. + +(c)Notices. Unless otherwise specifically provided herein, all notices, +consents, requests, demands and other communications required or permitted +hereunder: + +(i)shall be in writing; + +(ii)shall be sent by messenger, certified or registered mail/email, or +reliable express delivery service, to the appropriate address(es) set forth +below; and + +(iii)shall be deemed to have been given on the date of receipt by the +addressee, as evidenced by a receipt executed by the addressee (or a +responsible person in his or her office), the records of the Party delivering +such communication or a notice to the effect that such addressee refused to +claim or accept such communication, if sent by messenger, mail or express +delivery service. + +All such communications shall be sent to the following addresses or numbers, +or to such other addresses or numbers as any party may inform the others by +giving five days' prior notice: + +If to Webkul Software Pvt. Ltd.: + +Webkul Software Pvt. Ltd. +A-67, Sector 63, NOIDA – 201301, +Uttar Pradesh, India + +If to Licensee: +At the address mentioned by the Licensee +(at the time of placing order of generating Invoice) + +(d)Severability. It is the intent of the parties that the provisions of this +Agreement be enforced to the fullest extent permissible under the laws and +public policies of India in which enforcement hereof is sought. In +furtherance of the foregoing, each provision hereof shall be severable from +each other provision, and any provision hereof which is/ becomes unenforceable +shall be subject to the following: (i) if such provision is contrary to or +conflicts with any requirement of any statute, rule or regulation in effect, +then such requirement shall be incorporated into, or substituted for, such +unenforceable provision to the minimum extent necessary to make such provision +enforceable; (ii) the court, agency or arbitrator considering the matter is +hereby authorized to (or, if such court, agency or arbitrator is unwilling or +fails to do so, then the parties shall) amend such provision to the minimum +extent necessary to make such provision enforceable, and the parties hereby +consent to the entry of an order so amending such provision; and (iii) if +any such provision cannot be or is not reformed and made enforceable pursuant +to clause (i) or (ii) above, then such provision shall be ineffective to the +minimum extent necessary to make the remainder of this Agreement enforceable. +Any application of the foregoing provisions to any provision hereof shall not +effect the validity or enforceability of any other provision hereof. + +(e)By purchasing the Software, the Licensee acknowledge that it has read this +Agreement, and that it agrees to the content of the Agreement, its terms and +agree to use the Software in compliance with this Agreement. + +(f)The Licensor holds the sole copyright of the Software. The Software or any +portion thereof is a copyrightable matter and is liable to be protected by the +applicable laws. Copyright infringement in any manner can lead to prosecution +according to the current law. The Licensor reserves the right to revoke the +license of any user who is not holding any license or is holding an invalid +license. + +(g)This Agreement gives the right to use only one copy of the Software on one +domain solely for the own personal or business use of the Licensee, subject to +all the terms and conditions of this Agreement. A separate License has to be +purchased for each new Software installation. Any distribution of the Software +without the written consent of the Licensor (including non-commercial +distribution) is regarded as violation of this Agreement, and will entail +immediate termination of the Agreement and may invite liability, both civil +and criminal, as per applicable laws. + +(h)The Licensor reserves the rights to publish a selected list of users/ +Licensees of its Software, and no permission of any Licensee is needed in this +regard. The Licensee agrees that the Licensor may, in its sole discretion, +disclose or make available any information provided or submitted by the +Licensee or related to it under this Agreement to any judicial, +quasi-judicial, governmental, regulatory or any other authority as may be +required by the Licensor to co-operate and / or comply with any of their +orders, instructions or directions or to fulfill any requirements under +applicable Laws. + +(i)If the Licensee continues to use the Software even after the sending of the +notice by the Licensor for termination, the Licensee agree to accept an +injunction to restrain itself from its further use, and to pay all costs ( +including but not limited to reasonable attorney fees) to enforce injunction +or to revoke the License, and any damages suffered by the Licensor because of +the misuse of the Software by the Licensee. + + +12.ARBITRATION. + +If any dispute arises between the Licensor and the Licensee at any time, in +connection with the validity, interpretation, implementation or alleged breach +of any provision of this Agreement, the same shall be referred to a sole +Arbitrator who shall be an independent and neutral third party appointed +exclusively by the Licensor. The Licensee shall not object to the appointment +of the Arbitrator so appointed by the Licensor. The place of arbitration shall +be Delhi, India. The Arbitration & Conciliation Act, 1996 as amended by The +Arbitration & Conciliation (Amendment) Act, 2015, shall govern the +arbitration proceedings. The arbitration proceedings shall be held in the +English language. + + +This document is an electronic record in terms of Information Technology Act, +2000 and the amended provisions pertaining to electronic records in various +statutes as amended by the Information Technology Act, 2000. This electronic +record is generated by a computer system and does not require any physical or +digital signatures. \ No newline at end of file diff --git a/wk_backup_restore/__init__.py b/wk_backup_restore/__init__.py new file mode 100644 index 0000000..043ae26 --- /dev/null +++ b/wk_backup_restore/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +################################################################################# +# +# Copyright (c) 2017-Present Webkul Software Pvt. Ltd. () +# You should have received a copy of the License along with this program. +# If not, see +################################################################################# + +from . import models +from . import wizards +from . import controllers + +def pre_init_check(cr): + from odoo.service import common + from odoo.exceptions import UserError + version_info = common.exp_version() + server_serie =version_info.get('server_serie') + if server_serie != '18.0': + raise UserError('Module support Odoo series 18.0 found {}.'.format(server_serie)) + return True diff --git a/wk_backup_restore/__manifest__.py b/wk_backup_restore/__manifest__.py new file mode 100644 index 0000000..1f27579 --- /dev/null +++ b/wk_backup_restore/__manifest__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +################################################################################# +# Author : Webkul Software Pvt. Ltd. () +# Copyright(c): 2015-Present Webkul Software Pvt. Ltd. +# All Rights Reserved. +# +# +# +# This program is copyright property of the author mentioned above. +# You can`t redistribute it and/or modify it. +# +# +# You should have received a copy of the License along with this program. +# If not, see +################################################################################# +{ + "name" : "Odoo Database Backup", + "summary" : """Module provide feature to admin to take backups of his instance's database and later download them.""", + "category" : "Extra Tools", + "version" : "1.0.0", + "author" : "Webkul Software Pvt. Ltd.", + "license" : "Other proprietary", + "website" : "https://webkul.com/blog/odoo-data-backup-how-to-create-and-restore-data-in-odoo/", + "description" : """Module provide feature to admin to take backups of his instance's database and later download them.""", + "live_test_url" : "http://odoodemo.webkul.com/demo_feedback?module=wk_backup_restore", + "depends" : [ + 'base', + 'mail' + ], + "data" : [ + 'security/ir.model.access.csv', + 'wizards/backup_custom_message_wizard_view.xml', + 'wizards/backup_deletion_confirmation_view.xml', + 'views/backup_remote_server.xml', + 'data/backup_process_sequence.xml', + 'views/backup_process.xml', + 'data/backup_ignite_crone.xml', + 'views/menuitems.xml', + ], + "images" : ['static/description/Banner.png'], + "application" : True, + "installable" : True, + "currency" : "USD", + "pre_init_hook" : "pre_init_check", + "external_dependencies": {'python': ['python-crontab', 'paramiko']}, +} diff --git a/wk_backup_restore/controllers/__init__.py b/wk_backup_restore/controllers/__init__.py new file mode 100644 index 0000000..a03bfd0 --- /dev/null +++ b/wk_backup_restore/controllers/__init__.py @@ -0,0 +1 @@ +from . import controllers \ No newline at end of file diff --git a/wk_backup_restore/controllers/controllers.py b/wk_backup_restore/controllers/controllers.py new file mode 100644 index 0000000..e154d72 --- /dev/null +++ b/wk_backup_restore/controllers/controllers.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +################################################################################# +# +# Copyright (c) 2016-Present Webkul Software Pvt. Ltd. () +# See LICENSE file for full copyright and licensing details. +# License URL : +# +################################################################################# +import os +import logging +import datetime +import pytz +import shutil +import subprocess +import tempfile +import json + +import odoo +from odoo import http, _ +from odoo.http import request +from odoo.exceptions import AccessError, UserError +from odoo.tools.misc import exec_pg_environ, find_pg_tool + +_logger = logging.getLogger(__name__) + +class BackupController(http.Controller): + + + @http.route('/backupfile/download', type='http', auth='user') + def file_download(self, **kwargs): + file_path = request.httprequest.args.get('path') # The actual file path + backup_location = request.httprequest.args.get('backup_location') or 'local' + _logger.info(f"=====backup_location========= {backup_location} ====== file_path ====== {file_path}") + try: + # Read the file and return it as a response + file_data = None + with open(file_path, 'rb') as file: + file_data = file.read() + + # Set the response headers for file download + response = request.make_response(file_data) + response.headers['Content-Disposition'] = f"attachment; filename={file_path.split('/')[-1]}" + response.mimetype = 'application/octet-stream' + + # Delete the remote backup file from Main Server + if backup_location == 'remote': + os.remove(file_path) + + return response + except Exception as e: + _logger.info(f"======= Backup File Download Error ======= {e} ========") + raise UserError(e) + + + + @http.route('/saas/database/backup', type='http', auth="none", methods=['POST'], csrf=False) + def db_backup(self, **kwargs): + master_pwd = kwargs.get('master_pwd') + dbname = kwargs.get('name') + backup_format = kwargs.get('backup_format') or 'zip' + response = None + user = request.env['res.users'].sudo().browse([2]) + tz = pytz.timezone(user.tz) if user.tz else pytz.utc + time_now = pytz.utc.localize(datetime.datetime.now()).astimezone(tz) + ts = time_now.strftime("%m-%d-%Y-%H.%M.%S") + filename = "%s_%s.%s" % (dbname, ts, backup_format) + try: + odoo.service.db.check_super(master_pwd) + dump_stream = self.dump_db(dbname, None, backup_format) + response = request.make_response(dump_stream) + response.headers['Content-Disposition'] = f"attachment; filename={filename}" + response.mimetype = 'application/octet-stream' + except Exception as e: + error = "Database backup error: %s" % (str(e) or repr(e)) + _logger.exception('Database.backup --- %r', error) + response = request.make_response(error) + response.mimetype = 'text/html' + + response.headers['Backup-Filename'] = filename + response.headers['Backup-Time'] = time_now.strftime("%m-%d-%Y-%H:%M:%S") + return response + + + def dump_db_manifest(self, cr): + pg_version = "%d.%d" % divmod(cr._obj.connection.server_version / 100, 100) + cr.execute("SELECT name, latest_version FROM ir_module_module WHERE state = 'installed'") + modules = dict(cr.fetchall()) + manifest = { + 'odoo_dump': '1', + 'db_name': cr.dbname, + 'version': odoo.release.version, + 'version_info': odoo.release.version_info, + 'major_version': odoo.release.major_version, + 'pg_version': pg_version, + 'modules': modules, + } + return manifest + + def dump_db(self, db_name, stream, backup_format='zip'): + """Dump database `db` into file-like object `stream` if stream is None + return a file object with the dump """ + + _logger.info('DUMP DB: %s format %s', db_name, backup_format) + + cmd = [find_pg_tool('pg_dump'), '--no-owner', db_name] + env = exec_pg_environ() + + if backup_format == 'zip': + with tempfile.TemporaryDirectory() as dump_dir: + filestore = odoo.tools.config.filestore(db_name) + if os.path.exists(filestore): + shutil.copytree(filestore, os.path.join(dump_dir, 'filestore')) + with open(os.path.join(dump_dir, 'manifest.json'), 'w') as fh: + db = odoo.sql_db.db_connect(db_name) + with db.cursor() as cr: + json.dump(self.dump_db_manifest(cr), fh, indent=4) + cmd.insert(-1, '--file=' + os.path.join(dump_dir, 'dump.sql')) + subprocess.run(cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=True) + if stream: + odoo.tools.osutil.zip_dir(dump_dir, stream, include_dir=False, fnct_sort=lambda file_name: file_name != 'dump.sql') + else: + t=tempfile.TemporaryFile() + odoo.tools.osutil.zip_dir(dump_dir, t, include_dir=False, fnct_sort=lambda file_name: file_name != 'dump.sql') + t.seek(0) + return t + else: + cmd.insert(-1, '--format=c') + stdout = subprocess.Popen(cmd, env=env, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE).stdout + if stream: + shutil.copyfileobj(stdout, stream) + else: + return stdout diff --git a/wk_backup_restore/data/backup_ignite_crone.xml b/wk_backup_restore/data/backup_ignite_crone.xml new file mode 100644 index 0000000..2dced02 --- /dev/null +++ b/wk_backup_restore/data/backup_ignite_crone.xml @@ -0,0 +1,26 @@ + + + + + + + + + Backup Process Ignite Cron + + code + model.ignite_backup_server_crone() + 1 + hours + + + + Backup Process File Remove Cron + + code + model.remove_old_backups() + 1 + days + + + diff --git a/wk_backup_restore/data/backup_process_sequence.xml b/wk_backup_restore/data/backup_process_sequence.xml new file mode 100644 index 0000000..7cbc2f9 --- /dev/null +++ b/wk_backup_restore/data/backup_process_sequence.xml @@ -0,0 +1,15 @@ + + + + + + + + + Backup Process Name + backup.process + PROCESS + 3 + + + \ No newline at end of file diff --git a/wk_backup_restore/models/__init__.py b/wk_backup_restore/models/__init__.py new file mode 100644 index 0000000..5413971 --- /dev/null +++ b/wk_backup_restore/models/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +################################################################################# +# +# Copyright (c) 2017-Present Webkul Software Pvt. Ltd. () +# You should have received a copy of the License along with this program. +# If not, see +################################################################################# + +from . import backup_process +from . import backup_process_details +from . import backup_remote_server diff --git a/wk_backup_restore/models/backup_process.py b/wk_backup_restore/models/backup_process.py new file mode 100644 index 0000000..54e36b4 --- /dev/null +++ b/wk_backup_restore/models/backup_process.py @@ -0,0 +1,441 @@ +# -*- coding: utf-8 -*- +################################################################################# +# +# Copyright (c) 2016-Present Webkul Software Pvt. Ltd. () +# See LICENSE file for full copyright and licensing details. +# License URL : +# +################################################################################# + +from odoo import fields, api, models, tools +from odoo.exceptions import UserError +from odoo.tools.config import config + +from odoo.addons.wk_backup_restore.models.lib import manage_backup_crons, saas_client_backup +from datetime import datetime +import subprocess +import os +import paramiko + +import logging + +_logger = logging.getLogger(__name__) + + +LOCATION = [ + ('local', 'Local'), + ('remote', 'Remote Server'), +] + +CYCLE = [ + ('half_day', 'Twice a day'), + ('daily', 'Daily'), + ('weekly', 'Weekly'), + ('monthly', 'Monthly'), + ('yearly', 'Yearly'), +] + +STATE = [ + ('draft', 'Draft'), + ('confirm', 'Confirm'), + ('running', 'Running'), + ('cancel', 'Cancel') +] + +class BackupProcess(models.Model): + _name = "backup.process" + _description="Backup Process" + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = "id desc" + + def _default_db_name(self): + return self._cr.dbname + + name = fields.Char(string="Process Name", default='New', help="Display name for the backup process.") + frequency = fields.Integer(string="Frequency", default=1, help="Frequency for backuping the database.") + frequency_cycle = fields.Selection(selection=CYCLE, string="Frequency Cycle", help="Select frequency cycle of Database Backup.", tracking=True) + storage_path = fields.Char(string="Storage Path", help="The directory path where the backup files will be stored on server.", tracking=True) + backup_location = fields.Selection(selection=LOCATION, string="Backup Location", default="local", help="Server where the backup file will be stored.") + retention = fields.Integer(string="Backup Retention Count", default=7, help="Count of recent backups that will be retained after dropping old backups on server.") + # start_time = fields.Datetime(string="Backup Starting Time", help="Time from when the database backup can be started.") + db_name = fields.Char(string="Database Name", default=_default_db_name, help="Database used for the creating the backup.", tracking=True) + backup_starting_time = fields.Datetime(string="Backup Starting Time", help="Set Database Backup start date and time.") + state = fields.Selection(selection=STATE, default='draft', help="Current state of the backup process.") + update_requested = fields.Boolean(string="Update Requested", default=False, help="Checked if any backup is requested in the database backup.") + # master_pass = fields.Char(string="Master Password") + backup_details_ids = fields.One2many(comodel_name="backup.process.detail", inverse_name="backup_process_id", string="Backup Details", help="Details of the database backups that has been created.") + backup_format = fields.Selection([('zip', 'zip (includes filestore)'), ('dump', 'pg_dump custom format (without filestore)')], string="Backup Format", default="zip", help="Select the file format of the data backup file.", tracking=True) + enable_retention = fields.Boolean(string="Drop Old Backups", default=False, help="Check if you want to drop old backups stored on the server.") + remote_server_id = fields.Many2one(comodel_name="backup.remote.server", string="Backup Remote Server", domain=[('state', '=', 'validated')]) + + + @api.onchange('frequency_cycle') + def change_frequency_value(self): + """ + Method to change the value of frequency for Twice a day + """ + + if self.frequency_cycle == 'half_day': + self.frequency = 2 + else: + self.frequency = 1 + + @api.onchange('backup_location') + def change_backup_location(self): + """ + Method to check the validated remote servers + """ + if self.backup_location == 'remote': + backup_servers = self.env['backup.remote.server'].sudo().search([('state', '=', 'validated')]) + if not backup_servers: + raise UserError("No validated remote servers found. Please configure a remote server first!!") + self.remote_server_id = None + + + @api.constrains('retention') + def check_retention_value(self): + """ + Method to check the value of retention field + """ + + if self.enable_retention: + if self.retention < 1: + raise UserError("Backup Retention Count should be at least 1.") + + def call_backup_script(self, master_pass=None, port_number=None, url=None, db_user=None, db_password=None, kwargs={}): + """ + Called by create_backup_request method, defined below + Method to call script to create a cron for manage backups, + calling script require few arguments, some are passed in this method same are prepared below + """ + + db_user = db_user or config.get('db_user') + db_password = db_password or config.get('db_password') + module_path = tools.misc.file_path('wk_backup_restore') + module_path = module_path + '/models/lib/saas_client_backup.py' + backup_format = self.backup_format or "zip" + backup_location = self.backup_location + res = None + if hasattr(self,'_call_%s_backup_script'%backup_location):## if you want to update dictionary then you can define this function _call_{backup_location}_backup_script + res = getattr(self,'_call_%s_backup_script'%backup_location)(master_pass,port_number,url,db_user,db_password,backup_format, kwargs) + return res + + + def _call_local_backup_script(self, master_pass=None, port_number=None, url=None, db_user=None, db_password=None, backup_format="zip", kwargs={}): + """ + Called by call_backup_script method, defined above + Method to call script to create a cron for manage backups, + calling script require few arguments, some are passed in this method same are prepared below + """ + res = None + if self.backup_location == "local": + module_path = tools.misc.file_path('wk_backup_restore') + module_path = module_path + '/models/lib/saas_client_backup.py' + res = manage_backup_crons.add_cron(master_pass=master_pass, main_db=self._cr.dbname, db_name=self.db_name, backup_location=self.backup_location, frequency=self.frequency, frequency_cycle=self.frequency_cycle, storage_path=self.storage_path, url=url, db_user=db_user, db_password=db_password, process_id=self.id, module_path=module_path, backup_format=backup_format, backup_starting_time=self.backup_starting_time, kwargs=kwargs) + + if res.get('success'): + self.state = 'running' + return res + + + def _call_remote_backup_script(self, master_pass=None, port_number=None, url=None, db_user=None, db_password=None, backup_format="zip", kwargs=dict()): + """ + Called by call_backup_script method, defined above + Method to call script to create a cron for manage remote database backups, + calling script require few arguments, some are passed in this method same are prepared below + """ + res = None + if self.backup_location == "remote": + module_path = tools.misc.file_path('wk_backup_restore') + module_path = module_path + '/models/lib/saas_client_backup.py' + kwargs.update( + rhost = self.remote_server_id.sftp_host, + rport = self.remote_server_id.sftp_port, + ruser = self.remote_server_id.sftp_user, + rpass = self.remote_server_id.sftp_password, + temp_bkp_path = self.remote_server_id.temp_backup_dir, + ) + res = manage_backup_crons.add_cron(master_pass=master_pass, main_db=self._cr.dbname, db_name=self.db_name, backup_location=self.backup_location, frequency=self.frequency, frequency_cycle=self.frequency_cycle, storage_path=self.storage_path, url=url, db_user=db_user, db_password=db_password, process_id=self.id, module_path=module_path, backup_format=backup_format,backup_starting_time=self.backup_starting_time, kwargs=kwargs) + + if res.get('success'): + self.state = 'running' + return res + + + def update_backup_request(self): + """ + Method called from Cron, + Method called the script to update already created cron. + """ + + res = manage_backup_crons.update_cron(db_name=self.db_name, process_id=str(self.id), frequency=self.frequency, frequency_cycle=self.frequency_cycle) + if res.get('success'): + self.update_requested = False + + def create_backup_request(self): + """ + Called from the crone: + Method called the method to which call the crone script + Add 'master_passwd' in odoo conf file + """ + + master_pass = config.get('master_passwd') + if master_pass: + url = "localhost:"+str(config.get('http_port', '8069')) + return self.call_backup_script(master_pass=master_pass, url=url) + else: + _logger.info("------Error While Creating Backup Request--Master Password(master_passwd) is not set in conf file!!----------------") + + def remove_attached_cron(self): + """ + Called by the button over backup process page, + To cancel the Backup Process record and to call the delete cron script + """ + + if self.state == 'running': + res = manage_backup_crons.remove_cron(db_name=self.db_name, process_id=str(self.id), frequency=self.frequency, frequency_cycle=self.frequency_cycle) + else: + res = dict( + success = True + ) + if res.get('success'): + self.state = 'cancel' + return res + + @api.model + def ignite_backup_server_crone(self): + """ + Crone method to call functions either to create a new cron, or to update a existing one + """ + + current_time = datetime.now() + processes = self.env['backup.process'].sudo().search([('backup_starting_time', '<=', current_time), ('state', '=', 'confirm')]) + for process in processes: + process.create_backup_request() + upt_processes = self.env['backup.process'].sudo().search([('backup_starting_time', '<=', current_time), ('state', '=', 'running'), ('update_requested', '=', True)]) + for upt_process in upt_processes: + if upt_process.update_requested: + upt_process.update_backup_request() + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + vals['name'] = self.env['ir.sequence'].next_by_code('backup.process') + res = super(BackupProcess, self).create(vals) + return res + + def write(self, vals): + if self.state not in ['draft','cancel','confirm'] and self.backup_starting_time <= datetime.now() and not vals.get('update_requested') == False: + vals['update_requested'] = True + return super(BackupProcess, self).write(vals) + + + def unlink(self): + if self.state not in ['draft','cancel','confirm']: + raise UserError("Not allowed") + return super(BackupProcess, self).unlink() + + + def confirm_process(self): + """ + Called by the Confirm button over the backup process record + """ + + if self.state == 'draft': + # Raise error if master password is not set in odoo conf file + if not config.get('master_passwd', False): + raise UserError("Master password parameter(master_passwd) not set in Odoo conf file!!") + + # Creating the backup log file if doesn't exists + if not os.path.exists(manage_backup_crons.LOG_FILE_PATH): + _logger.info("========== Creating Backup Log File ==========") + fp = open(manage_backup_crons.LOG_FILE_PATH, 'x') + fp.close() + + if self.backup_location == 'remote': + self.validate_remote_backup() + self.state ="confirm" + + def cancel_process(self): + """ + Called by the Cancel button over the backup process record + """ + + if self.state in ['draft','confirm']: + self.state ="cancel" + + @api.model + def remove_old_backups(self): + """ + Cron method to call functions to remove the backup file of the backup processes + """ + + processes = self.env['backup.process'].sudo().search([('state', '=', 'running'),('enable_retention', '=', True)]) + for rec in processes: + details_ids = rec.backup_details_ids.filtered(lambda d: d.status == "Success").sorted(key=lambda p:p.id) + if details_ids: + end_index = len(details_ids) - rec.retention + if end_index>0: + updated_details_ids = details_ids[:end_index] + rec.remove_backup_files(updated_details_ids) + + def remove_backup_files(self, bkp_details_ids): + """ + Method to check if the backup file exist, and if exist then remove that backup file. + Also, updates the status and the message of the backup process details. + + Args: + bkp_details_ids ([object]): [all the backup process ids whose backup file needs to be deleted.] + """ + try: + msg = None + for bkp in bkp_details_ids: + backup_location = self.backup_location + if hasattr(self,'_remove_%s_backup_files'%backup_location):## if you want to update dictionary then you can define this function _remove_{backup_location}_backup_files + msg = getattr(self,'_remove_%s_backup_files'%backup_location)(bkp) + _logger.info("---- %r -- ", msg) + return True + except Exception as e: + _logger.error("Database backup remove error: " + str(e)) + return False + + + def _remove_local_backup_files(self, bkp_details_id): + """ + Method to check if the backup file exist on the main server, + and if exist then remove that backup file. + """ + msg = None + if os.path.exists(bkp_details_id.url): + res = os.remove(bkp_details_id.url) + msg = 'Database backup dropped successfully at ' + datetime.now().strftime("%m-%d-%Y-%H:%M:%S") + " after retention." + bkp_details_id.message = msg + bkp_details_id.status = "Dropped" + else: + msg = "Database backup file doesn't exists." + bkp_details_id.message = msg + bkp_details_id.status = "Failure" + + return msg + + + def _remove_remote_backup_files(self, bkp_details_id): + """ + Method to check if the backup file exist on the remote backup server, + and if exist then remove that backup file. + """ + msg = None + ssh_obj = self.login_remote() + if self.check_remote_backup_existance(ssh_obj, bkp_details_id.url): + sftp = ssh_obj.open_sftp() + sftp.remove(bkp_details_id.url) + sftp.close() + msg = 'Database backup dropped successfully at ' + datetime.now().strftime("%m-%d-%Y-%H:%M:%S") + " after retention from remote server." + bkp_details_id.message = msg + bkp_details_id.status = "Dropped" + else: + msg = "Database backup file doesn't exists on remote server." + bkp_details_id.message = msg + bkp_details_id.status = "Failure" + + return msg + + def login_remote(self): + """ + Method to login to the remote backup server using SSH. + + Returns: + [Object]: [Returns SSh object if connected successfully to the remote server.] + """ + try: + ssh_obj = paramiko.SSHClient() + ssh_obj.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh_obj.connect(hostname=self.remote_server_id.sftp_host, username=self.remote_server_id.sftp_user, password=self.remote_server_id.sftp_password, port=self.remote_server_id.sftp_port) + return ssh_obj + except Exception as e: + _logger.info(f"==== Exception while connecting to remote server ==== {e} ===") + return False + + def test_host_connection(self): + if self.remote_server_id: + response = self.validate_remote_backup() + if response: + message = self.env['backup.custom.message.wizard'].create({'message':"Connection successful!"}) + action = self.env.ref('wk_backup_restore.action_backup_custom_message_wizard').read()[0] + action['res_id'] = message.id + return action + + + def validate_remote_backup(self): + """ + Method to validate the remote backup process. + It checks the connection to remote server along with the existance of backup + storage path on the remote server. + """ + ssh_obj = self.login_remote() + if ssh_obj: + backup_dir = self.storage_path + cmd = "ls %s"%(backup_dir) + check_path = self.execute_on_remote_shell(ssh_obj,cmd) + if check_path and not check_path.get('status'): + raise UserError(f"Storage path doesn't exist on remote server. Please create the mentioned backup path on the remote server. Error: {check_path.get('message')}") + + cmd = f"touch {backup_dir}/test.txt" + create_file = self.execute_on_remote_shell(ssh_obj,cmd) + if create_file and not create_file.get('status'): + raise UserError(f"The mentioned ssh user doesn't have rights to create file. Please provide required permissions on the default backup path. Error: {create_file.get('message')}") + else: + cmd = f"rm {backup_dir}/test.txt" + delete_file = self.execute_on_remote_shell(ssh_obj,cmd) + if delete_file and delete_file.get('status'): + _logger.info("======== Backup Directory Permissions Checked Successfully =========") + + else: + raise UserError("Couldn't connect to the remote server.") + + return True + + + def check_remote_backup_existance(self, ssh_obj, bkp_path): + """ + Method to check the existance of the backup file on the remote server. + Args: + ssh_obj ([object]): [SSH Object of the remote server.] + bkp_path ([object]): [Path of the backup file on the remote server.] + """ + cmd = "ls -f %s"%(bkp_path) + check_path = self.execute_on_remote_shell(ssh_obj,cmd) + if check_path and not check_path.get('status'): + _logger.error(f"-----------Database Backup file '{bkp_path}' doesn't exist on remote server.--------") + return False + return True + + + + def execute_on_remote_shell(self, ssh_obj,command): + """ + Method to execute the command on the remote server. + """ + _logger.info(command) + response = dict() + try: + ssh_stdin, ssh_stdout, ssh_stderr = ssh_obj.exec_command(command) + # _logger.info(ssh_stdout.readlines()) + res = ssh_stdout.readlines() + _logger.info("execute_on_remote_shell res: %r", res) + _logger.info("execute_on_remote_shell err: ") + err = ssh_stderr.readlines() + _logger.info(err) + if err: + response['status'] = False + response['message'] = err + return response + response['status'] = True + response['result'] = res + return response + except Exception as e: + _logger.info("+++ERROR++",command) + _logger.info("++++++++++ERROR++++",e) + response['status'] = False + response['message'] = e + return response diff --git a/wk_backup_restore/models/backup_process_details.py b/wk_backup_restore/models/backup_process_details.py new file mode 100644 index 0000000..0ad26fc --- /dev/null +++ b/wk_backup_restore/models/backup_process_details.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +################################################################################# +# +# Copyright (c) 2016-Present Webkul Software Pvt. Ltd. () +# See LICENSE file for full copyright and licensing details. +# License URL : +# +################################################################################# + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +from . lib import check_connectivity + + +import logging +import base64 +import os + +_logger = logging.getLogger(__name__) + + +class ProcessBackupDetail(models.Model): + _name = 'backup.process.detail' + _description = "Backup Process Details" + _order = "id desc" + + name = fields.Char(string="Name") + file_name = fields.Char(string="File Name") + backup_process_id = fields.Many2one(string="Backup Process Id", comodel_name="backup.process") + file_path = fields.Char(string="File Path") + url = fields.Char(string="Url") + backup_date_time = fields.Datetime(string="Backup Time") + status = fields.Char(string="Status") + message = fields.Char(string="Message") + backup_location = fields.Selection(string="Backup Location", related="backup_process_id.backup_location", help="Server where the backup file will be stored.") + + def download_db_file(self): + """ + Call by the download button over every backup detail record. + Method download the zip file of backup, + """ + try: + backup_file_path = None + download_url = None + if self.backup_location == 'local': + backup_file_path = self.url + download_url = f"/backupfile/download?path={backup_file_path}&backup_location=local" + else: + backup_copy_status = self.get_remote_backup_file() + if backup_copy_status: + backup_file_path = self.backup_process_id.remote_server_id.temp_backup_dir+"/"+self.file_name + download_url = f"/backupfile/download?path={backup_file_path}&backup_location=remote" + else: + raise UserError("Cannot download backup file from remote server. Follow logs for more details.") + + if self.status == "Success" and os.path.exists(backup_file_path): + return { + 'type': 'ir.actions.act_url', + 'url': download_url, + 'target': 'new', + } + else: + raise UserError("Backup doesn't exists.") + except Exception as e: + raise UserError(f"Error Occured: {e}") + + + def get_remote_backup_file(self): + """ + Method to copy the backup file from the remote server to the main server + + Returns: + [Boolean]: True in case file is successfully copied or False + """ + try: + + host_server = self.backup_process_id.remote_server_id.get_server_details() + temp_path = self.backup_process_id.remote_server_id.temp_backup_dir + response = check_connectivity.ishostaccessible(host_server) + + if not response.get('status'): + return False + + ssh_obj = response.get('result') + sftp = ssh_obj.open_sftp() + sftp.get(self.url, temp_path+'/'+self.file_name) + sftp.close() + _logger.info("======== Backup file successfully copied to the local server. ===========") + return True + except Exception as e: + _logger.info(f"======= Exception while copying the backup file from the remote server ======= {e} ") + return False + + def unlink_confirmation(self): + for rec in self: + if rec.status=="Success": + msg = """ Warning: After Deleting this record you will no longer be able to download the backup file associated with this record. However, after deletion the backup will still remain on server. + Are you sure you want to delete this backup record? + """ + partial_id = self.env['backup.deletion.confirmation'].create({'backup_id': rec.id, 'message': msg}) + return { + 'type': 'ir.actions.act_window', + 'name': 'Deletion Confirmation', + 'view_mode': 'form', + 'res_model': 'backup.deletion.confirmation', + 'res_id': partial_id.id, + 'target': 'new', + } + else: + rec.unlink() diff --git a/wk_backup_restore/models/backup_remote_server.py b/wk_backup_restore/models/backup_remote_server.py new file mode 100644 index 0000000..73f3b58 --- /dev/null +++ b/wk_backup_restore/models/backup_remote_server.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +################################################################################# +# +# Copyright (c) 2016-Present Webkul Software Pvt. Ltd. () +# See LICENSE file for full copyright and licensing details. +# License URL : +# +################################################################################# + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +from . lib import check_connectivity + + +import logging +import base64 +import os + +_logger = logging.getLogger(__name__) + +STATE = [ + ('draft', "Draft"), + ('validated', 'Validated'), +] + +class BackupRemoteServer(models.Model): + _name = 'backup.remote.server' + _description="Backup Remote Server" + + name = fields.Char(string="Name", help="Name of the backup remote server") + sftp_host = fields.Char(string="Remote SFTP Host", help="SFTP host for establishing connection to the backup remote server") + sftp_port = fields.Char(string="Remote SFTP Port", default="22", help="SFTP port for establishing connection to the backup remote server") + sftp_user = fields.Char(string="SFTP User", help="SFTP user for establishing connection to the backup remote server") + sftp_password = fields.Char(string="SFTP Password", help="SFTP password for establishing connection to the backup remote server") + + state = fields.Selection(selection=STATE, string="State", default="draft", help="State of the backup remote server") + active = fields.Boolean(string="Active", default=True) + temp_backup_dir = fields.Char(string="Temporary Backup Directory", help="The temporary backup path where the backups are stored before moving to the remote server. The temporary backup directory must be present on the main server along with the appropriate permissions.") + def_backup_dir = fields.Char(string="Default Remote Backup Directory", help="The default directory path on the remote server where the backups of the saas client instances will be stored. The directory must have appropriate permissions.") + + + + + def test_host_connection(self): + """ + Method to check Host connection: called by the button 'Test Connection' + """ + + for obj in self: + response = obj.check_host_connected_call() + if response.get('status'): + message = self.env['backup.custom.message.wizard'].create({'message':"Connection successful!"}) + action = self.env.ref('wk_backup_restore.action_backup_custom_message_wizard').read()[0] + action['res_id'] = message.id + return action + else: + raise UserError(response.get('message')) + + + def check_host_connected_call(self): + """ + Method to call the script to check host connectivity, + return response dict as per the output. + Called from 'test_host_connection' and 'set_validated' + """ + response = dict( + status=True, + message='Success' + ) + host_server = self.get_server_details() + try: + response = check_connectivity.ishostaccessible(host_server) + if response and response.get('status'): + _logger.info("======= Remote Server Connection Successful ======") + ssh_obj = response.get('result') + backup_dir = self.def_backup_dir + cmd = "ls %s"%(backup_dir) + check_path = self.execute_on_remote_shell(ssh_obj,cmd) + if check_path and not check_path.get('status'): + raise UserError(f"Storage path doesn't exist on remote server. Please create the mentioned backup path on the remote server. Error: {check_path.get('message')}") + + cmd = f"touch {backup_dir}/test.txt" + create_file = self.execute_on_remote_shell(ssh_obj,cmd) + if create_file and not create_file.get('status'): + raise UserError(f"The mentioned ssh user doesn't have rights to create file. Please provide required permissions on the default backup path. Error: {create_file.get('message')}") + else: + cmd = f"rm {backup_dir}/test.txt" + delete_file = self.execute_on_remote_shell(ssh_obj,cmd) + if delete_file and delete_file.get('status'): + _logger.info("======== Backup Directory Permissions Checked Successfully =========") + except Exception as e: + _logger.info(f"------ EXCEPTION WHILE TESTING THE REMOTE SERVER CONNECTION ---- {e} ------") + response['status'] = False + response['message'] = e + return response + + @api.model + def get_server_details(self): + """ + Method created to return value of the host server as dict, + Called from check_host_connected_call method in the complete process + """ + host_server = dict( + host=self.sftp_host, + port=self.sftp_port, + user=self.sftp_user, + password=self.sftp_password, + ) + return host_server + + + def set_validated(self): + for obj in self: + response = obj.check_host_connected_call() + if response.get('status'): + obj.state = 'validated' + else: + raise UserError(response.get('message')) + + def reset_to_draft(self): + for obj in self: + bkp_processes = self.env['backup.process'].search([('remote_server_id', '=', obj.id), ('backup_location', '=', 'remote'), ('state', 'in', ['confirm', 'running'])]) + if bkp_processes: + raise UserError("This Remote Server has some active Backup Process(es)!") + obj.state = 'draft' + + def execute_on_remote_shell(self, ssh_obj,command): + """ + Method to execute the command on the remote server. + """ + _logger.info(command) + response = dict() + try: + ssh_stdin, ssh_stdout, ssh_stderr = ssh_obj.exec_command(command) + res = ssh_stdout.readlines() + _logger.info("execute_on_remote_shell res: %r", res) + err = ssh_stderr.readlines() + _logger.info("execute_on_remote_shell err: ") + _logger.info(err) + if err: + response['status'] = False + response['message'] = err + return response + response['status'] = True + response['result'] = res + return response + except Exception as e: + _logger.info("+++ERROR++",command) + _logger.info("++++++++++ERROR++++",e) + response['status'] = False + response['message'] = e + return response diff --git a/wk_backup_restore/models/lib/check_connectivity.py b/wk_backup_restore/models/lib/check_connectivity.py new file mode 100644 index 0000000..de57cc3 --- /dev/null +++ b/wk_backup_restore/models/lib/check_connectivity.py @@ -0,0 +1,23 @@ +import os, time, sys +import re, shutil +import logging +import paramiko +_logger = logging.getLogger(__name__) + +def ishostaccessible(details): + response = dict( + status=True, + message='Success' + ) + try: + ssh_obj = paramiko.SSHClient() + ssh_obj.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + _logger.info("Database Backup In check_connectivity scipt at Line {}".format(17)) + ssh_obj.connect(hostname = details['host'], username = details['user'], password = details['password'], port = details['port']) + response['result'] = ssh_obj + return response + except Exception as e: + _logger.info("Couldn't connect remote %r"%e) + response['status'] = False + response['message'] = e + return response diff --git a/wk_backup_restore/models/lib/manage_backup_crons.py b/wk_backup_restore/models/lib/manage_backup_crons.py new file mode 100644 index 0000000..a0f92f2 --- /dev/null +++ b/wk_backup_restore/models/lib/manage_backup_crons.py @@ -0,0 +1,208 @@ +from crontab import CronTab +import datetime +import logging +#import getpass +import os +import pwd + +PYTHON_ENV = "/usr/bin/python3" +LOG_FILE_PATH = "/var/log/odoo/backup_cron.log" +# BACKUP_SCRIPT_PATH = "/odoo/webkul_addons/wk_backup_restore/models/lib/saas_client_backup.py" + +_logger = logging.getLogger(__name__) + +class Cronjob: + def __init__(self, create_time, frequency, frequency_cycle): + self.command = '' + self.create_time = create_time + self.frequency = frequency + self.frequency_cycle = frequency_cycle + self.user = pwd.getpwuid(os.getuid())[0] + self.cron = CronTab(user=self.user) + + def create_command(self, masterpswd, url, main_db, db_name, db_user, db_password, process_id, backup_location, storage_path, module_path, backup_format="zip",kwargs = {}): + _logger.info("===============CREATING BACKUP CMD=======================") + command = "{} {} --mpswd '{}' --url {} --dbname {} --maindb {} --dbuser {} --dbpassword '{}' --processid {} --bkploc {} --path {} --backup_format {} >> {} 2>&1".format(PYTHON_ENV, module_path, masterpswd, url, db_name, main_db, db_user, db_password, process_id, backup_location, storage_path, backup_format, LOG_FILE_PATH) + + extra_args = "" + for key, val in kwargs.items(): + extra_args += "--{} '{}' ".format(key, val) + + if extra_args: + initial_command = command.split(">>") + command = f"{initial_command[0]}{extra_args} >>{initial_command[1]}" + + self.command = command + + def set_time_for_cron(self): + _logger.info(self.frequency_cycle) + self.job.minute.on(0) + date, time = self.create_time.split(',') + m, d, y = map(int, date.split('/')) + h, mi, s = map(int, time.split(':')) + if self.frequency_cycle == 'half_day': + h%=12 + self.job.hour.during(h, 23).every(12) + self.job.minute.on(mi) + self.job.dow.on() + self.job.dom.on() + return True + elif self.frequency_cycle == 'daily': + if self.frequency == 1: + self.job.hour.on(h) + self.job.minute.on(mi) + self.job.dow.on() + self.job.dom.on() + return True + else: + return False + elif self.frequency_cycle == 'weekly' or self.frequency_cycle == 'week': + _logger.info("time %r" %h) + day_of_week = datetime.datetime(y,m,d).weekday() + 1 + if self.frequency == 1: + self.job.dow.on(day_of_week) + self.job.hour.on(h) + self.job.minute.on(0) + self.job.day.on() + return True + else: + return False + elif self.frequency_cycle == 'monthly' or self.frequency_cycle == 'month': + if self.frequency == 1: + self.job.dom.on(d) + self.job.hour.on(h) + self.job.minute.on(mi) + return True + else: + return False + elif self.frequency_cycle == 'yearly' or self.frequency_cycle == 'year': + if self.frequency == 1: + self.job.dom.on(d) + self.job.hour.on(h) + self.job.minute.on(mi) + self.job.month.on(m) + return True + else: + return False + + + def create_cronjob(self): + job = self.cron.new( + command=self.command) + self.job = job + self.set_time_for_cron() + return True + + def write_crontab(self): + try: + self.cron.write() + return { + "success": True, + "msg": None + } + except Exception as e: + return { + "success": False, + "msg": str(e) + } + + def remove_cron(self, process_id): + process_id = "--processid "+process_id+" " + jobs = list(self.list_cronjobs(process_id)) + if len(jobs) == 1: + _logger.info("%s"%(jobs[0])) + self.cron.remove(jobs[0]) + + def list_cronjobs(self, search_keyword=None): + if search_keyword: + return self.cron.find_command(search_keyword) + return self.cron.lines + + def update_cronjob(self, process_id): + _logger.info("Updating Job") + process_id = "--processid "+process_id+" " + jobs = list(self.list_cronjobs(process_id)) + if len(jobs) == 1: + _logger.info("%s"%(jobs[0])) + self.job = jobs[0] + res = self.set_time_for_cron() + if res: + return { + 'success': True, + 'msg': self.job + } + else: + return { + 'success': False, + 'msg': 'Invalid Time.' + } + else: + # More than one cron for same client. + return { + 'success': True, + 'msg': jobs + } + + + # set_cron_for_new_client(masterpwd, url, dbname, backup_location, interval, create_time, storage_path, cycle) +def add_cron(master_pass, url, main_db, db_name, db_user, db_password, process_id, backup_location, frequency, frequency_cycle, storage_path, module_path, backup_format="zip", backup_starting_time=None, kwargs={}): + _logger.info(locals()) + create_time = backup_starting_time.strftime("%m/%d/%Y, %H:%M:%S") + cj = Cronjob(create_time, frequency, frequency_cycle) + cj.create_command(master_pass, url, main_db, db_name, db_user, db_password, process_id, backup_location, storage_path, module_path, backup_format, kwargs) + _logger.info("%s"%(cj.command)) + cj.create_cronjob() + wc = cj.write_crontab() + if wc['success']: + _logger.info("Added Job Successfully") + else: + _logger.info("ERROR: %s"%(wc['msg'])) + + return wc + +def update_cron(db_name, process_id, frequency, frequency_cycle): + create_time = datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S") + cj = Cronjob(create_time, frequency, frequency_cycle) + cj.command = db_name + uc = cj.update_cronjob(process_id) + if uc['success'] == False: + return uc + else: + wc = cj.write_crontab() + if wc['success']: + _logger.info("Updated Job Successfully") + else: + _logger.info("ERROR: %s"%(wc['msg'])) + + return wc + +def remove_cron(db_name, process_id, frequency, frequency_cycle): + create_time = datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S") + cj = Cronjob(create_time, frequency, frequency_cycle) + cj.command = db_name + cj.remove_cron(process_id) + wc = cj.write_crontab() + if wc['success']: + _logger.info("Removed Job Successfully") + else: + _logger.info("ERROR: %s"%(wc['msg'])) + + return wc + +if __name__ == '__main__': + import os + master_pass = 'CnvvV46UGZb2=N' + url = 'http://192.168.5.125/' + main_db = 'postgres' + db_name = 'test_backup_crone.odoo-saas.webkul.com' + db_user = 'postgres' + db_password = 'postgres' + process_id = 1234 + backup_location = 'local' + frequency_cycle = 'weekly' # monthly, weekly, yearly, half_day. + frequency = 2 if frequency_cycle == 'half_day' else 1 + module_path = '/opt/webkul_addons/wk_backup_restore/models/lib/saas_client_backup.py' + storage_path = os.getcwd() + update_cron(db_name, frequency, frequency_cycle) + # add_cron(master_pass, url, main_db, db_name, db_user, db_password, process_id, backup_location, frequency, frequency_cycle, storage_path, module_path) + _logger.info(update_cron(db_name, frequency, frequency_cycle)) diff --git a/wk_backup_restore/models/lib/saas_client_backup.py b/wk_backup_restore/models/lib/saas_client_backup.py new file mode 100644 index 0000000..79d6dd0 --- /dev/null +++ b/wk_backup_restore/models/lib/saas_client_backup.py @@ -0,0 +1,384 @@ +# curl -X POST -F 'master_pwd=abcd' -F 'name=xyz' -F 'backup_format=zip' -o /path/xyz.zip http://localhost:8069/web/database/backup +import requests +import argparse +import os +import datetime +import psycopg2 +import paramiko +import subprocess +from urllib.parse import urlparse +import json + + + + +class BackupStorage(): + def __init__(self): + self.client_url = "" + self.ssh_obj = None + self.saas_ssh_obj = None + self.msg = "" + self.filename = "" + self.backup_time = None + self.backup_file_path = "" + self.remote_backup_file_path = "" + self.temp_backup_file_path = "" + + def init_parser(self): + """ + Method to initialize parser for command line arguments, + and return parser object. + """ + parser = argparse.ArgumentParser(description='Process some arguments.') + parser.add_argument('--mpswd', action='store', + help='Master password Odoo') + parser.add_argument('--url', action='store', + help='saas client url') + parser.add_argument('--dbname', action='store', + help='name of database to backup') + parser.add_argument('--maindb', action='store', + help='name of main database') + parser.add_argument('--dbuser', action='store', + help='username of main database') + parser.add_argument('--dbpassword', action='store', + help='password of main database') + parser.add_argument('--processid', action='store', + help='process id') + parser.add_argument('--bkploc', action='store', + help='backup location local, dedicated, s3') + parser.add_argument('--path', action='store', + help='Backup Path') + parser.add_argument('--backup_format', action='store', + help='Backup Type') + + parser.add_argument('--rhost', action='store', + help='Remote Hostname') + parser.add_argument('--rport', action='store', + help='Remote Port') + parser.add_argument('--ruser', action='store', + help='Remote User') + parser.add_argument('--rpass', action='store', + help='Remote Password') + + parser.add_argument('--temp_bkp_path', action='store', + help='Temporary Backup Directory') + + # Arguments related to SaaS Kit Backup module + parser.add_argument('--is_remote_client', action='store', + help='Is Remote SaaS Client') + + + return parser + + def database_entry(self, main_db, db_user, db_password, db_name, file_name, process_id, file_path, url, backup_date_time, status, message, kwargs={}): + """ + Method to insert created backup details in the database. + """ + try: + if db_user == "False" or db_password == "False": + connection = psycopg2.connect(database=main_db) + else: + connection = psycopg2.connect(user=db_user, password=db_password, host="127.0.0.1", port="5432", database=main_db) + except Exception as e: + print(e) + print('Exited') + exit(0) + + try: + file_path = file_path.replace('//', '/') + url = url.replace('//', '/') + # Connect to database + QUERY = "INSERT INTO backup_process_detail (name, file_name, backup_process_id, file_path, url, backup_date_time, status, message) VALUES (%s, %s, %s, %s, %s, %s, %s, %s);" + RECORD = (db_name, file_name, process_id, file_path, url, backup_date_time, status, message) + cursor = connection.cursor() + print("PostgreSQL server information") + print(connection.get_dsn_parameters(), "\n") + cursor.execute(QUERY, RECORD) + connection.commit() + count = cursor.rowcount + print(count, "Record inserted") + except Exception as e: + print(e) + finally: + if connection: + cursor.close() + connection.close() + print("Postgresql Connection Closed") + + def login_backup_remote(self, args): + """ + Method to login to remote backup server. + """ + try: + ssh_obj = paramiko.SSHClient() + ssh_obj.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh_obj.connect(hostname=args.rhost, username=args.ruser, password=args.rpass,port=args.rport) + self.ssh_obj = ssh_obj + except Exception as e: + print("Couldn't connect to remote backup server.", e) + raise Exception("Couldn't connect to remote backup server.") + + def execute_on_remote_shell(self, ssh_obj,command): + """ + Method to execute commands on shell of remote server. + """ + response = dict() + try: + ssh_stdin, ssh_stdout, ssh_stderr = ssh_obj.exec_command(command) + print("execute_on_remote_shell out: ") + res = ssh_stdout.readlines() + print(res) + print("execute_on_remote_shell err: ") + err = ssh_stderr.readlines() + print(err) + if err: + raise Exception(err) + response['status'] = True + response['result'] = res + return response + except Exception as e: + print("+++ERROR++",command) + print("++++++++++ERROR++++",e) + response['status'] = False + response['message'] = e + return response + + def check_remote_backup_path(self, args, backup_dir): + """ + Method to check remote backup path. + """ + response = dict(status=False) + try: + self.login_backup_remote(args) + cmd = "ls %s"%(backup_dir) + check_path = self.execute_on_remote_shell(self.ssh_obj ,cmd) + if check_path and not check_path.get('status'): + print("Error while checking the path of remote directory - ", check_path.get('message')) + raise Exception("Error while checking the path of remote directory - "+check_path.get('message')) + if check_path and not check_path.get('result'): + cmd = "mkdir -p %s; chmod -R 777 %s"%(backup_dir, backup_dir) + upd_permission = self.execute_on_remote_shell(self.ssh_obj,cmd) + if upd_permission and not upd_permission.get('status'): + print("Error while creating directory and updating permissions - ", check_path.get('message')) + raise Exception("Cannot create remote directory and update permissions.") + response.update(status=True) + except Exception as e: + print("Error: Creating Backup Directory") + response.update(message=e) + return response + + def create_client_url(self, url): + """ + Method to create client url for creating the backups. + """ + client_url = "" + if urlparse(url).scheme not in ['http','https']: + client_url = 'http://' + url + \ + ('/' if url[-1] != '/' else '') + else: + client_url = url + ('/' if url[-1] != '/' else '') + + client_url += 'saas/database/backup' + return client_url + + + def store_backup_file(self, args, kwargs): + """ + Method to store backup file on the local server in the mentioned path. + """ + res = dict(status=False) + data = { + 'master_pwd': args.mpswd, + 'name': args.dbname, + 'backup_format': args.backup_format or "zip" + } + + client_url = self.client_url + backup_dir = kwargs.get('backup_dir') + try: + filename = None + backup_time = None + backup_file_path = None + with requests.post(client_url, data=data, stream=True) as response: + response.raise_for_status() + filename = response.headers.get('Backup-Filename', '') + backup_time = response.headers.get('Backup-Time', datetime.datetime.now().strftime("%m-%d-%Y-%H:%M:%S")) + backup_file_path = os.path.join(backup_dir, filename) + + if response.headers.get('Content-Disposition'): + with open(backup_file_path, 'wb') as file: + for chunk in response.iter_content(chunk_size=1024): + if chunk: + file.write(chunk) + else: + raise Exception(response.content.decode()) + + msg = 'Database Backup Successful at ' + str(backup_time) + res.update(status=True, filename=filename, backup_time=backup_time, backup_file_path=backup_file_path) + except Exception as e: + res.update(message=e) + + return res + + + def manage_backup_files(self, args): + """ + Method to manage the backup files on either local server or remote server or any cloud server + """ + vals = dict() + backup_dir = os.path.join(args.path, 'backups') + response = dict(status=False) + self.client_url = self.create_client_url(args.url) + try: + vals.update(backup_dir=backup_dir) + backup_location = args.bkploc + if hasattr(self,'_create_%s_backup'%backup_location):## if you want to update dictionary then you can define this function _call_{backup_location}_backup_script + response = getattr(self,'_create_%s_backup'%backup_location)(args, vals) + + msg = 'Database Backup Successful at ' + str(self.backup_time) + self.database_entry(args.maindb, args.dbuser, args.dbpassword, args.dbname, self.filename, args.processid, backup_dir+'/', self.backup_file_path, datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), status="Success", message=msg) + response.update(status=True, message=msg) + except Exception as e: + msg = 'Failed at ' + str(self.backup_time or datetime.datetime.now()) + ' ' + str(e) + self.database_entry(args.maindb, args.dbuser, args.dbpassword, args.dbname, self.filename, args.processid, backup_dir+'/', self.backup_file_path if self.backup_file_path else self.remote_backup_file_path if self.remote_backup_file_path else '', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), status="Failure", message=msg) + response.update(status=False, message=msg) + + return response + + + def _create_local_backup(self, args, vals): + """ + Method to create backup on local server. + It copies the local backup file to remote saas server. + """ + response = dict(status=False) + temp_backup_dir = None + backup_dir = vals.get('backup_dir') + if not os.path.exists(backup_dir) and not eval(args.is_remote_client if args.is_remote_client else 'False'): + os.makedirs(backup_dir) + + if args.is_remote_client and eval(args.is_remote_client): + temp_backup_dir = args.temp_bkp_path + vals.update(backup_dir=temp_backup_dir) + + backup_store_res = self.store_backup_file(args, vals) + if backup_store_res and not backup_store_res.get('status'): + raise Exception(backup_store_res.get('message')) + + self.filename = backup_store_res.get('filename') + self.backup_time = backup_store_res.get('backup_time') + self.backup_file_path = backup_store_res.get('backup_file_path') + + if args.is_remote_client and eval(args.is_remote_client): + self.backup_file_path = os.path.join(backup_dir, self.filename) + self.temp_bkp_file_path = backup_store_res.get('backup_file_path') + response = self._create_saas_remote_backup(args) + + return response + + def _create_remote_backup(self, args, vals): + """ + Method to create the temporary database backup on main server and store it on the remote server. + The temporary DB Backup file will be deleted after storing it on the remote server. + """ + response = dict(status=False) + backup_dir = vals.get('backup_dir') + temp_backup_dir = args.temp_bkp_path + vals.update(backup_dir=temp_backup_dir) + check_path_res = self.check_remote_backup_path(args, backup_dir) + if check_path_res and not check_path_res.get('status'): + raise Exception(check_path_res.get('message')) + backup_store_res = self.store_backup_file(args, vals) + if backup_store_res and not backup_store_res.get('status'): + raise Exception(backup_store_res.get('message')) + + self.filename = backup_store_res.get('filename') + self.backup_time = backup_store_res.get('backup_time') + self.temp_backup_file_path = backup_store_res.get('backup_file_path') + self.remote_backup_file_path = os.path.join(backup_dir, self.filename) + self.backup_file_path = self.remote_backup_file_path + + sftp = self.ssh_obj.open_sftp() + sftp.put(self.temp_backup_file_path, self.remote_backup_file_path) + sftp.close() + + cmd = f"ls -f {self.remote_backup_file_path}" + + # Checking if the backup file is successfully copied to remote server + check_file_exist = self.execute_on_remote_shell(self.ssh_obj,cmd) + if check_file_exist and check_file_exist.get("status"): + print("\nBackup file successfully copied to the remote server.") + print("remote backup_file_path --->", self.remote_backup_file_path) + + # DELETE the temporary backup file from the Main Server + if os.path.exists(self.temp_backup_file_path): + os.remove(self.temp_backup_file_path) + print("\nBackup file successfully deleted from the Main Server.") + + response.update(status=True) + return response + else: + print("\nBackup file doesn't successfully moved to the remote server.") + raise Exception("Backup file couldn't be moved to remote server.") + + + + def login_saas_remote(self, remote): + """ + Method to login to the remote SaaS server using SSH. + """ + try: + ssh_obj = paramiko.SSHClient() + ssh_obj.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh_obj.connect(hostname=remote.get('host'), username=remote.get('user'), password=remote.get('password'),port=remote.get('port')) + self.saas_ssh_obj = ssh_obj + except Exception as e: + print("Couldn't connect remote SaaS server: ", e) + raise Exception("Couldn't connect to remote SaaS server.") + + + def _create_saas_remote_backup(self, args): + """ + This method is crated to make the compatibility with SaaS Kit Backup module. + This method will copy the local backup file to the remote saas server. + The temporary backup file will be deleted after storing it on the remote saas server. + """ + saas_url = 'http://localhost:8069/remote/server/creds' + saas_data = { + 'backup_process_id': int(args.processid) + } + response = dict(status=False) + try: + # Getting the host server creds of the remote saas server + with requests.post(saas_url, data=saas_data, stream=True) as saas_response: + saas_response.raise_for_status() + resp = json.loads((saas_response.content).decode()) + + # Uploading the backup file from the main server to remote saas server + self.login_saas_remote(resp.get('host_server')) + if self.saas_ssh_obj: + saas_sftp = self.saas_ssh_obj.open_sftp() + saas_sftp.put(self.temp_bkp_file_path, self.backup_file_path) + + # Removing the temporary backup on the main server + if os.path.exists(self.temp_bkp_file_path): + saas_sftp.remove(self.temp_bkp_file_path) + saas_sftp.close() + print("Local Backup File Successfully Copied to the Remote SaaS Server") + + response.update(status=True) + except Exception as e: + print("Exception while copying the local backup to the remote saas server") + raise Exception("Local Backup file couldn't be moved to remote saas server.") + + return response + + + +if __name__ == '__main__': + backup_storage = BackupStorage() + parser = backup_storage.init_parser() + args = parser.parse_args() + print(backup_storage.manage_backup_files(args)) + + diff --git a/wk_backup_restore/security/ir.model.access.csv b/wk_backup_restore/security/ir.model.access.csv new file mode 100644 index 0000000..d7192c8 --- /dev/null +++ b/wk_backup_restore/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_backup_process,backup.process,model_backup_process,,1,1,1,1 +access_backup_process_detail,backup.process.detail,model_backup_process_detail,,1,1,1,1 +access_backup_deletion_confirmation_wizard,backup.deletion.confirmation,model_backup_deletion_confirmation,,1,1,1,1 +access_backup_remote_server,access.backup.remote.server,model_backup_remote_server,,1,1,1,1 +access_backup_custom_message_wizard,access.backup.custom.message.wizard,model_backup_custom_message_wizard,,1,1,1,1 diff --git a/wk_backup_restore/static/description/Banner.gif b/wk_backup_restore/static/description/Banner.gif new file mode 100644 index 0000000..b8b3f69 Binary files /dev/null and b/wk_backup_restore/static/description/Banner.gif differ diff --git a/wk_backup_restore/static/description/Banner.png b/wk_backup_restore/static/description/Banner.png new file mode 100644 index 0000000..9ec4bf6 Binary files /dev/null and b/wk_backup_restore/static/description/Banner.png differ diff --git a/wk_backup_restore/static/description/backup_db.png b/wk_backup_restore/static/description/backup_db.png new file mode 100644 index 0000000..7f98733 Binary files /dev/null and b/wk_backup_restore/static/description/backup_db.png differ diff --git a/wk_backup_restore/static/description/backup_ss_1.png b/wk_backup_restore/static/description/backup_ss_1.png new file mode 100644 index 0000000..16b8b8c Binary files /dev/null and b/wk_backup_restore/static/description/backup_ss_1.png differ diff --git a/wk_backup_restore/static/description/backup_ss_1_bkp.jpg b/wk_backup_restore/static/description/backup_ss_1_bkp.jpg new file mode 100644 index 0000000..9de34c6 Binary files /dev/null and b/wk_backup_restore/static/description/backup_ss_1_bkp.jpg differ diff --git a/wk_backup_restore/static/description/backup_ss_2.png b/wk_backup_restore/static/description/backup_ss_2.png new file mode 100644 index 0000000..fa9aebe Binary files /dev/null and b/wk_backup_restore/static/description/backup_ss_2.png differ diff --git a/wk_backup_restore/static/description/backup_ss_2_bkp.jpg b/wk_backup_restore/static/description/backup_ss_2_bkp.jpg new file mode 100644 index 0000000..352ac15 Binary files /dev/null and b/wk_backup_restore/static/description/backup_ss_2_bkp.jpg differ diff --git a/wk_backup_restore/static/description/backup_ss_3.png b/wk_backup_restore/static/description/backup_ss_3.png new file mode 100644 index 0000000..e06c710 Binary files /dev/null and b/wk_backup_restore/static/description/backup_ss_3.png differ diff --git a/wk_backup_restore/static/description/backup_ss_3_bkp.jpg b/wk_backup_restore/static/description/backup_ss_3_bkp.jpg new file mode 100644 index 0000000..999b153 Binary files /dev/null and b/wk_backup_restore/static/description/backup_ss_3_bkp.jpg differ diff --git a/wk_backup_restore/static/description/backup_ss_4.png b/wk_backup_restore/static/description/backup_ss_4.png new file mode 100644 index 0000000..2994313 Binary files /dev/null and b/wk_backup_restore/static/description/backup_ss_4.png differ diff --git a/wk_backup_restore/static/description/backup_ss_4_bkp.jpg b/wk_backup_restore/static/description/backup_ss_4_bkp.jpg new file mode 100644 index 0000000..3d089ae Binary files /dev/null and b/wk_backup_restore/static/description/backup_ss_4_bkp.jpg differ diff --git a/wk_backup_restore/static/description/backup_ss_5.png b/wk_backup_restore/static/description/backup_ss_5.png new file mode 100644 index 0000000..53c50c6 Binary files /dev/null and b/wk_backup_restore/static/description/backup_ss_5.png differ diff --git a/wk_backup_restore/static/description/backup_ss_6.png b/wk_backup_restore/static/description/backup_ss_6.png new file mode 100644 index 0000000..d1e08d7 Binary files /dev/null and b/wk_backup_restore/static/description/backup_ss_6.png differ diff --git a/wk_backup_restore/static/description/backup_ss_7.png b/wk_backup_restore/static/description/backup_ss_7.png new file mode 100644 index 0000000..0cf3db2 Binary files /dev/null and b/wk_backup_restore/static/description/backup_ss_7.png differ diff --git a/wk_backup_restore/static/description/check.png b/wk_backup_restore/static/description/check.png new file mode 100644 index 0000000..8dba264 Binary files /dev/null and b/wk_backup_restore/static/description/check.png differ diff --git a/wk_backup_restore/static/description/guideicon.png b/wk_backup_restore/static/description/guideicon.png new file mode 100644 index 0000000..6bb3695 Binary files /dev/null and b/wk_backup_restore/static/description/guideicon.png differ diff --git a/wk_backup_restore/static/description/icon.png b/wk_backup_restore/static/description/icon.png new file mode 100644 index 0000000..ec7a9c3 Binary files /dev/null and b/wk_backup_restore/static/description/icon.png differ diff --git a/wk_backup_restore/static/description/index.html b/wk_backup_restore/static/description/index.html new file mode 100644 index 0000000..793b844 --- /dev/null +++ b/wk_backup_restore/static/description/index.html @@ -0,0 +1,358 @@ + + + + + +
+
+
+
+
+ +
+
+
+
+
+
+ Supported Editions : +
+
+
+
+ +
+
On Premise
+
+
+
+
+
+ +
+
Odoo.sh
+
+
+
+
+
+ +
+
Odoo Online
+
+
+
+
+
+
+
+
+
+

+ Odoo Database Backup +

+
+ Ease to Manage Backup Process for Odoo Database! +
+
+
+
+
+

+ Odoo Database Backup Module allows you to create a backup process for your instance to manage backup of database.You can configure a backup process to create backup as hourly, daily, weekly, monthly. +

+
+
+

Information

+
+ user-guide +
+ User Guide +
+
+
+ https://webkul.com/blog/odoo-data-backup-how-to-create-and-restore-data-in-odoo/ +
+ + + + + + + + + +
+
+
+
+ +
+
+
+
+
+
+

+ Odoo is a perfect software to manage your complete business. Moreover, it helps you to save and manage different business processes like product management, inventory management, order management, and more. +

+
+
+

+ When it comes to enhancing the protection of your data, you must include a backup solution that will cover all the parts of data protection that your Odoo misses. Moreover, backup software ensures critical data is protected properly through backup, and is always available via restore. +

+
+
+

+ Now, you can easily manage backup processes for your Odoo Instance. Odoo Database Backup Module allows you to create a backup process for your instance to manage backup of database. You can configure a backup process to create backup as hourly, daily, weekly, monthly. +

+
+
+
+
+
+
+ +
+
+
+
+
+
+ + Perks of Backup Solution in Odoo +
+
+
+
+
+
+ Odoo backup software refers to technology that stores and protects data that products create. +
+
+
  • + Smart businesses use SaaS backup and restore to make sure that important data is safe. +
  • +
  • + Regular data backups help businesses know the problems with their data and find opportunities to fix them. +
  • +
  • + You do not need to suffer the disruption and downtime of restoring whole data sets to earlier states when you can identify and restore only the problematic data. +
  • +
  • + You'll be able to delete old backups after retention of some specific count of recent backups on server. +
  • +
  • + Now, you'll be able to backup on the local server as well as remote server. +
  • +
    +
    + + Note: The module has been thoroughly tested on the Ubuntu operating system. + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +

    Detailed Features List

    +
    +
    +
    +
    +
    +
      +
    • Admin can take backup of db on daily/Weekly/monthly/yearly by configuring the backup process.
    • +
    • Backup will store on the local server as well as on remote as zip/dump.
    • +
    • Admin can also download the backuped zip from the record.
    • +
    • You can also update/cancel the process easily.
    • +
    • Admin can set the retention period for the backup files.
    • +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + Create and Configure the Local Backup Process with correct and desired details. +
    +
    +
    +
    + Not found +
    +
    +
    +
    +
    +
    + After configure the Backup Process, confirm it to start the backup operations. +
    +
    +
    +
    + Not found +
    +
    +
    +
    +
    +
    + When module starts taking your database backup, it will listed in the record. You can download the zip file of any db backup by clicking on Download button. +
    +
    +
    +
    + Not found +
    +
    +
    +
    +
    +
    + Later, you can also cancel the Backup Process to stop taking backups. +
    +
    +
    +
    + Not found +
    +
    +
    +
    +
    +
    + Configure and validate the remote backup server with correct and desired details. +
    +
    +
    +
    + Not found +
    +
    +
    +
    +
    +
    + Configure and Confirm the Remote Backup Process with correct and desired details. +
    +
    +
    +
    + Not found +
    +
    +
    +
    +
    +
    + When module starts taking your database backup, it will listed in the record. You can download the zip file of any db backup by clicking on Download button. +
    +
    +
    +
    + Not found +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +

    Help and Support

    +

    Get Immediate support for any of your query

    +
    +
    +

    You will get 90 days free support for any doubt, queries, and bug fixing (excluding data recovery) or any type of issue related to this module.

    +
    +
    +
    +
    + mail +
    +
    +

    Write a mail to us:

    + support@webkul.com +

    Any queries or want any extra features? Just drop a mail to our support.

    +
    + +
    +
    +
    +
    +
    + support-icon +
    +
    +

    Get in touch with our Expert:

    + https://webkul.uvdesk.com/en/customer/create-ticket/ +

    Have any technical queries, want extra features, or anything else? Our team is here to answer all your questions. Just Raise A Support Ticket.

    +
    +
    +
    +
    +
    + +
    +
    + + + +
    +
    +
    + + diff --git a/wk_backup_restore/static/description/mail.png b/wk_backup_restore/static/description/mail.png new file mode 100644 index 0000000..8f5913e Binary files /dev/null and b/wk_backup_restore/static/description/mail.png differ diff --git a/wk_backup_restore/static/description/point.svg b/wk_backup_restore/static/description/point.svg new file mode 100644 index 0000000..2ecdf5e --- /dev/null +++ b/wk_backup_restore/static/description/point.svg @@ -0,0 +1,3 @@ + + + diff --git a/wk_backup_restore/static/description/support-icon.png b/wk_backup_restore/static/description/support-icon.png new file mode 100644 index 0000000..9c1f7e4 Binary files /dev/null and b/wk_backup_restore/static/description/support-icon.png differ diff --git a/wk_backup_restore/static/description/webkul_icon.png b/wk_backup_restore/static/description/webkul_icon.png new file mode 100644 index 0000000..ca4496a Binary files /dev/null and b/wk_backup_restore/static/description/webkul_icon.png differ diff --git a/wk_backup_restore/static/description/wrong.png b/wk_backup_restore/static/description/wrong.png new file mode 100644 index 0000000..4646834 Binary files /dev/null and b/wk_backup_restore/static/description/wrong.png differ diff --git a/wk_backup_restore/static/description/youtube2.png b/wk_backup_restore/static/description/youtube2.png new file mode 100644 index 0000000..cd372e8 Binary files /dev/null and b/wk_backup_restore/static/description/youtube2.png differ diff --git a/wk_backup_restore/views/backup_process.xml b/wk_backup_restore/views/backup_process.xml new file mode 100644 index 0000000..5f4bb18 --- /dev/null +++ b/wk_backup_restore/views/backup_process.xml @@ -0,0 +1,119 @@ + + + + + + + + + Backup Process Form View + backup.process + form + +
    +
    +
    + +
    +
    +
    +
    + + + + + +
    +
    + + + + + + +
    +
    + + + +
    + Note: +
      +
    • After enabling Drop Database backup you need to set backup retention count.
    • +
    • The Backup Retention Count is the number of backups you wish to keep.
    • +
    +
    +
    +
    + + + + + + + + + + + +