You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

283 lines
10 KiB

[9.0][MIG] auto_backup (#526) * Update english pot file Added all the new fields and sentences. This will be the template for translations. * Dutch translations Full translation of the module to Dutch * Chinese translations Add Chinese translations to the module. Written by talway. * Changes chinese translation Changed some translations * Full German translations Thanks to Martin Schmid! * Typo fix could'nt > couldn't * Flemish translations Flemish translations are identical to Dutch. * Open FTP session on the last moment possible Used to open fp = open(file_path,'wb') a few lines before it was needed. This shouldn't be too much of a problem but opening and closing it right after eachother keeps the session open for less time and there are less chances on failure. * Porting module to OCA 8.0 * [FIX] module * [FIX] bug logger --> _logger call [FIX] Flake8 [RM] useless files * [FIX] Readme.rst * [FIX] defaults value * [FIX] rebased commit * [RM] description index.html * [RF] porting to new api [FIX] schedule_backup method [IMP] IT translation [IMP] tests * [FIX] test * [FIX] flake8 * [IMP] deps in travis.yml [FIX] flake8 * [FIX] flake8 and pylint * [FIX] name of file * [FIX] autoremove method [FIX] Contributors * [FIX] mail.tempale seems not work in cron task, replaced with direct call of mail.mail * [FIX] Readme * [FIX] review remarks * [FIX] handled ssl hosts [FIX] Flake8 * [FIX] handled ssl hosts [FIX] Flake8 * [FIX] fixed, last review remarks * [FIX] travis lint check * [FIX] backup only local db , beacause xmlrpc call of dump cause memory leak * [RM] useless field * [FIX] check_dd method [ADD] test case improved * [auto_backup] Refactor. - Follow template README. - Remove HTML README. - Move models to models folder. - Model and view file names follow guidelines. - Unused methods cleanup. - Remove unneeded `.pot` file. - Fix permissons. - Follow PEP8 in names everywhere. - Set more descriptive field names. - Disable backups for other databases, for security. - Remove db name from generated file, for easier cleanup. - EAFP logic everywhere. - More descriptive name. - Data files moved to YAML, with cleaner ir.cron record creation. - Add permissions for db.backup model. - Icons. - Update tests with new format. - Storage method is a selectable, for easier extensibility. - Instead of custom mailing, it just has a mail thread where you can subscribe. - Should fix almost all comments in https://github.com/OCA/server-tools/pull/203. * Update english pot file Added all the new fields and sentences. This will be the template for translations. * Dutch translations Full translation of the module to Dutch * Chinese translations Add Chinese translations to the module. Written by talway. * Changes chinese translation Changed some translations * Full German translations Thanks to Martin Schmid! * Typo fix could'nt > couldn't * Flemish translations Flemish translations are identical to Dutch. * Open FTP session on the last moment possible Used to open fp = open(file_path,'wb') a few lines before it was needed. This shouldn't be too much of a problem but opening and closing it right after eachother keeps the session open for less time and there are less chances on failure. * Porting module to OCA 8.0 * [FIX] module * [FIX] bug logger --> _logger call [FIX] Flake8 [RM] useless files * [FIX] Readme.rst * [FIX] defaults value * [FIX] rebased commit * [RM] description index.html * [RF] porting to new api [FIX] schedule_backup method [IMP] IT translation [IMP] tests * [FIX] test * [FIX] flake8 * [IMP] deps in travis.yml [FIX] flake8 * [FIX] flake8 and pylint * [FIX] name of file * [FIX] autoremove method [FIX] Contributors * [FIX] mail.tempale seems not work in cron task, replaced with direct call of mail.mail * [FIX] Readme * [FIX] review remarks * [FIX] handled ssl hosts [FIX] Flake8 * [FIX] handled ssl hosts [FIX] Flake8 * [FIX] fixed, last review remarks * [FIX] travis lint check * [FIX] backup only local db , beacause xmlrpc call of dump cause memory leak * [RM] useless field * [FIX] check_dd method [ADD] test case improved * [auto_backup] Refactor. - Follow template README. - Remove HTML README. - Move models to models folder. - Model and view file names follow guidelines. - Unused methods cleanup. - Remove unneeded `.pot` file. - Fix permissons. - Follow PEP8 in names everywhere. - Set more descriptive field names. - Disable backups for other databases, for security. - Remove db name from generated file, for easier cleanup. - EAFP logic everywhere. - More descriptive name. - Data files moved to YAML, with cleaner ir.cron record creation. - Add permissions for db.backup model. - Icons. - Update tests with new format. - Storage method is a selectable, for easier extensibility. - Instead of custom mailing, it just has a mail thread where you can subscribe. - Should fix almost all comments in https://github.com/OCA/server-tools/pull/203. * Reduce headers. This respects the upstream license choice (GPL/AGPL) but reduces verbosity. It would be ideal to have everything under AGPL though. * Fix view format. * Add shortcut to execute backups from the "More" menu. * Avoid duplicated backups. * Make sure you don't backup inside the filestore folder. The filestore is saved in the backup, so if you save the backup in the filestore, you'd end up with a huge backup that includes itself and the universe may collapse. * [FIX] This was removing all databases. * FIX License type * OCA Transbot updated translations from Transifex * OCA Transbot updated translations from Transifex * OCA Transbot updated translations from Transifex * [FIX] auto_backup: bad reference to field sftp_private_key (#423) Bump module version to 8.0.1.0.1 * [FIX] auto_backup: Empty dump using sftp backup option (#432) * [FIX] logger db_backup for pysftp (#419) * OCA Transbot updated translations from Transifex * OCA Transbot updated translations from Transifex * OCA Transbot updated translations from Transifex * OCA Transbot updated translations from Transifex * [FIX] remove en.po that was erroneously created by transbot * [MIG] auto_backup: Migrate to v9 * Add self.ensure_ones * Add test coverage * [ADD] auto_backup: Test coverage * compute_name * check_folder * action_sftp_test_connection * action_backup - sftp * action_backup_all * sftp_connection * filename
8 years ago
  1. # -*- coding: utf-8 -*-
  2. # © 2004-2009 Tiny SPRL (<http://tiny.be>).
  3. # © 2015 Agile Business Group <http://www.agilebg.com>
  4. # © 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis
  5. # License AGPL-3.0 or later (http://www.gnu.org/licenses/gpl.html).
  6. import os
  7. import shutil
  8. import traceback
  9. from contextlib import contextmanager
  10. from datetime import datetime, timedelta
  11. from glob import iglob
  12. from openerp import exceptions, models, fields, api, _, tools
  13. from openerp.service import db
  14. import logging
  15. _logger = logging.getLogger(__name__)
  16. try:
  17. import pysftp
  18. except ImportError: # pragma: no cover
  19. _logger.debug('Cannot import pysftp')
  20. class DbBackup(models.Model):
  21. _name = 'db.backup'
  22. _inherit = "mail.thread"
  23. _sql_constraints = [
  24. ("name_unique", "UNIQUE(name)", "Cannot duplicate a configuration."),
  25. ("days_to_keep_positive", "CHECK(days_to_keep >= 0)",
  26. "I cannot remove backups from the future. Ask Doc for that."),
  27. ]
  28. name = fields.Char(
  29. string="Name",
  30. compute="_compute_name",
  31. store=True,
  32. help="Summary of this backup process",
  33. )
  34. folder = fields.Char(
  35. default=lambda self: self._default_folder(),
  36. oldname="bkp_dir",
  37. help='Absolute path for storing the backups',
  38. required=True
  39. )
  40. days_to_keep = fields.Integer(
  41. oldname="daystokeep",
  42. required=True,
  43. default=0,
  44. help="Backups older than this will be deleted automatically. "
  45. "Set 0 to disable autodeletion.",
  46. )
  47. method = fields.Selection(
  48. selection=[("local", "Local disk"), ("sftp", "Remote SFTP server")],
  49. default="local",
  50. help="Choose the storage method for this backup.",
  51. )
  52. sftp_host = fields.Char(
  53. string='SFTP Server',
  54. oldname="sftpip",
  55. help=(
  56. "The host name or IP address from your remote"
  57. " server. For example 192.168.0.1"
  58. )
  59. )
  60. sftp_port = fields.Integer(
  61. string="SFTP Port",
  62. default=22,
  63. oldname="sftpport",
  64. help="The port on the FTP server that accepts SSH/SFTP calls."
  65. )
  66. sftp_user = fields.Char(
  67. string='Username in the SFTP Server',
  68. oldname="sftpusername",
  69. help=(
  70. "The username where the SFTP connection "
  71. "should be made with. This is the user on the external server."
  72. )
  73. )
  74. sftp_password = fields.Char(
  75. string="SFTP Password",
  76. oldname="sftppassword",
  77. help="The password for the SFTP connection. If you specify a private "
  78. "key file, then this is the password to decrypt it.",
  79. )
  80. sftp_private_key = fields.Char(
  81. string="Private key location",
  82. help="Path to the private key file. Only the Odoo user should have "
  83. "read permissions for that file.",
  84. )
  85. @api.model
  86. def _default_folder(self):
  87. """Default to ``backups`` folder inside current server datadir."""
  88. return os.path.join(
  89. tools.config["data_dir"],
  90. "backups",
  91. self.env.cr.dbname)
  92. @api.multi
  93. @api.depends("folder", "method", "sftp_host", "sftp_port", "sftp_user")
  94. def _compute_name(self):
  95. """Get the right summary for this job."""
  96. for rec in self:
  97. if rec.method == "local":
  98. rec.name = "%s @ localhost" % rec.folder
  99. elif rec.method == "sftp":
  100. rec.name = "sftp://%s@%s:%d%s" % (
  101. rec.sftp_user, rec.sftp_host, rec.sftp_port, rec.folder)
  102. @api.multi
  103. @api.constrains("folder", "method")
  104. def _check_folder(self):
  105. """Do not use the filestore or you will backup your backups."""
  106. for s in self:
  107. if (s.method == "local" and
  108. s.folder.startswith(
  109. tools.config.filestore(self.env.cr.dbname))):
  110. raise exceptions.ValidationError(
  111. _("Do not save backups on your filestore, or you will "
  112. "backup your backups too!"))
  113. @api.multi
  114. def action_sftp_test_connection(self):
  115. """Check if the SFTP settings are correct."""
  116. try:
  117. # Just open and close the connection
  118. with self.sftp_connection():
  119. raise exceptions.Warning(_("Connection Test Succeeded!"))
  120. except (pysftp.CredentialException, pysftp.ConnectionException):
  121. _logger.info("Connection Test Failed!", exc_info=True)
  122. raise exceptions.Warning(_("Connection Test Failed!"))
  123. @api.multi
  124. def action_backup(self):
  125. """Run selected backups."""
  126. backup = None
  127. filename = self.filename(datetime.now())
  128. successful = self.browse()
  129. # Start with local storage
  130. for rec in self.filtered(lambda r: r.method == "local"):
  131. with rec.backup_log():
  132. # Directory must exist
  133. try:
  134. os.makedirs(rec.folder)
  135. except OSError:
  136. pass
  137. with open(os.path.join(rec.folder, filename),
  138. 'wb') as destiny:
  139. # Copy the cached backup
  140. if backup:
  141. with open(backup) as cached:
  142. shutil.copyfileobj(cached, destiny)
  143. # Generate new backup
  144. else:
  145. db.dump_db(self.env.cr.dbname, destiny)
  146. backup = backup or destiny.name
  147. successful |= rec
  148. # Ensure a local backup exists if we are going to write it remotely
  149. sftp = self.filtered(lambda r: r.method == "sftp")
  150. if sftp:
  151. if backup:
  152. cached = open(backup)
  153. else:
  154. cached = db.dump_db(self.env.cr.dbname, None)
  155. with cached:
  156. for rec in sftp:
  157. with rec.backup_log():
  158. with rec.sftp_connection() as remote:
  159. # Directory must exist
  160. try:
  161. remote.makedirs(rec.folder)
  162. except pysftp.ConnectionException:
  163. pass
  164. # Copy cached backup to remote server
  165. with remote.open(
  166. os.path.join(rec.folder, filename),
  167. "wb") as destiny:
  168. shutil.copyfileobj(cached, destiny)
  169. successful |= rec
  170. # Remove old files for successful backups
  171. successful.cleanup()
  172. @api.model
  173. def action_backup_all(self):
  174. """Run all scheduled backups."""
  175. return self.search([]).action_backup()
  176. @api.multi
  177. @contextmanager
  178. def backup_log(self):
  179. """Log a backup result."""
  180. try:
  181. _logger.info("Starting database backup: %s", self.name)
  182. yield
  183. except:
  184. _logger.exception("Database backup failed: %s", self.name)
  185. escaped_tb = tools.html_escape(traceback.format_exc())
  186. self.message_post(
  187. "<p>%s</p><pre>%s</pre>" % (
  188. _("Database backup failed."),
  189. escaped_tb),
  190. subtype=self.env.ref("auto_backup.failure"))
  191. else:
  192. _logger.info("Database backup succeeded: %s", self.name)
  193. self.message_post(_("Database backup succeeded."))
  194. @api.multi
  195. def cleanup(self):
  196. """Clean up old backups."""
  197. now = datetime.now()
  198. for rec in self.filtered("days_to_keep"):
  199. with rec.cleanup_log():
  200. oldest = self.filename(now - timedelta(days=rec.days_to_keep))
  201. if rec.method == "local":
  202. for name in iglob(os.path.join(rec.folder,
  203. "*.dump.zip")):
  204. if os.path.basename(name) < oldest:
  205. os.unlink(name)
  206. elif rec.method == "sftp":
  207. with rec.sftp_connection() as remote:
  208. for name in remote.listdir(rec.folder):
  209. if (name.endswith(".dump.zip") and
  210. os.path.basename(name) < oldest):
  211. remote.unlink(name)
  212. @api.multi
  213. @contextmanager
  214. def cleanup_log(self):
  215. """Log a possible cleanup failure."""
  216. self.ensure_one()
  217. try:
  218. _logger.info("Starting cleanup process after database backup: %s",
  219. self.name)
  220. yield
  221. except:
  222. _logger.exception("Cleanup of old database backups failed: %s")
  223. escaped_tb = tools.html_escape(traceback.format_exc())
  224. self.message_post(
  225. "<p>%s</p><pre>%s</pre>" % (
  226. _("Cleanup of old database backups failed."),
  227. escaped_tb),
  228. subtype=self.env.ref("auto_backup.failure"))
  229. else:
  230. _logger.info("Cleanup of old database backups succeeded: %s",
  231. self.name)
  232. @api.model
  233. def filename(self, when):
  234. """Generate a file name for a backup.
  235. :param datetime.datetime when:
  236. Use this datetime instead of :meth:`datetime.datetime.now`.
  237. """
  238. return "{:%Y_%m_%d_%H_%M_%S}.dump.zip".format(when)
  239. @api.multi
  240. def sftp_connection(self):
  241. """Return a new SFTP connection with found parameters."""
  242. self.ensure_one()
  243. params = {
  244. "host": self.sftp_host,
  245. "username": self.sftp_user,
  246. "port": self.sftp_port,
  247. }
  248. _logger.debug(
  249. "Trying to connect to sftp://%(username)s@%(host)s:%(port)d",
  250. extra=params)
  251. if self.sftp_private_key:
  252. params["private_key"] = self.sftp_private_key
  253. if self.sftp_password:
  254. params["private_key_pass"] = self.sftp_password
  255. else:
  256. params["password"] = self.sftp_password
  257. return pysftp.Connection(**params)