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.

282 lines
10 KiB

  1. # -*- coding: utf-8 -*-
  2. ##############################################################################
  3. # OpenERP, Open Source Management Solution
  4. # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
  5. # Copyright 2015 Agile Business Group <http://www.agilebg.com>
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. ##############################################################################
  21. import os
  22. import shutil
  23. import tempfile
  24. import traceback
  25. from contextlib import contextmanager
  26. from datetime import datetime, timedelta
  27. from glob import iglob
  28. from openerp import exceptions, models, fields, api, _, tools
  29. from openerp.service import db
  30. import logging
  31. _logger = logging.getLogger(__name__)
  32. try:
  33. import pysftp
  34. except ImportError:
  35. _logger.warning('Cannot import pysftp')
  36. class DbBackup(models.Model):
  37. _name = 'db.backup'
  38. _inherit = "mail.thread"
  39. _sql_constraints = [
  40. ("days_to_keep_positive", "CHECK(days_to_keep >= 0)",
  41. "I cannot remove backups from the future. Ask Doc for that."),
  42. ]
  43. name = fields.Char(
  44. string="Name",
  45. compute="_compute_name",
  46. store=True,
  47. help="Summary of this backup process",
  48. )
  49. folder = fields.Char(
  50. default=lambda self: self._default_folder(),
  51. oldname="bkp_dir",
  52. help='Absolute path for storing the backups',
  53. required=True
  54. )
  55. days_to_keep = fields.Integer(
  56. oldname="daystokeep",
  57. required=True,
  58. default=0,
  59. help="Backups older than this will be deleted automatically. "
  60. "Set 0 to disable autodeletion.",
  61. )
  62. method = fields.Selection(
  63. selection=[("local", "Local disk"), ("sftp", "Remote SFTP server")],
  64. default="local",
  65. help="Choose the storage method for this backup.",
  66. )
  67. sftp_host = fields.Char(
  68. string='SFTP Server',
  69. oldname="sftpip",
  70. help=(
  71. "The host name or IP address from your remote"
  72. " server. For example 192.168.0.1"
  73. )
  74. )
  75. sftp_port = fields.Integer(
  76. string="SFTP Port",
  77. default=22,
  78. oldname="sftpport",
  79. help="The port on the FTP server that accepts SSH/SFTP calls."
  80. )
  81. sftp_user = fields.Char(
  82. string='Username in the SFTP Server',
  83. oldname="sftpusername",
  84. help=(
  85. "The username where the SFTP connection "
  86. "should be made with. This is the user on the external server."
  87. )
  88. )
  89. sftp_password = fields.Char(
  90. string="SFTP Password",
  91. oldname="sftppassword",
  92. help="The password for the SFTP connection. If you specify a private "
  93. "key file, then this is the password to decrypt it.",
  94. )
  95. sftp_private_key = fields.Char(
  96. string="Private key location",
  97. help="Path to the private key file. Only the Odoo user should have "
  98. "read permissions for that file.",
  99. )
  100. @api.model
  101. def _default_folder(self):
  102. """Default to ``backups`` folder inside current database datadir."""
  103. return os.path.join(
  104. tools.config.filestore(self.env.cr.dbname),
  105. "backups")
  106. @api.multi
  107. @api.depends("folder", "method", "sftp_host", "sftp_port", "sftp_user")
  108. def _compute_name(self):
  109. """Get the right summary for this job."""
  110. for rec in self:
  111. if rec.method == "local":
  112. rec.name = "%s @ localhost" % rec.folder
  113. elif rec.method == "sftp":
  114. rec.name = "sftp://%s@%s:%d%s" % (
  115. rec.sftp_user, rec.sftp_host, rec.sftp_port, rec.folder)
  116. @api.multi
  117. def action_sftp_test_connection(self):
  118. """Check if the SFTP settings are correct."""
  119. try:
  120. # Just open and close the connection
  121. with self.sftp_connection():
  122. raise exceptions.Warning(_("Connection Test Succeeded!"))
  123. except (pysftp.CredentialException, pysftp.ConnectionException):
  124. _logger.info("Connection Test Failed!", exc_info=True)
  125. raise exceptions.Warning(_("Connection Test Failed!"))
  126. @api.multi
  127. def action_backup(self):
  128. """Run selected backups."""
  129. backup = None
  130. filename = self.filename(datetime.now())
  131. successful = self.browse()
  132. # Start with local storage
  133. for rec in self.filtered(lambda r: r.method == "local"):
  134. with rec.backup_log():
  135. # Directory must exist
  136. try:
  137. os.makedirs(rec.folder)
  138. except OSError:
  139. pass
  140. with open(os.path.join(rec.folder, filename),
  141. 'wb') as destiny:
  142. # Copy the cached backup
  143. if backup:
  144. with open(backup) as cached:
  145. shutil.copyfileobj(cached, destiny)
  146. # Generate new backup
  147. else:
  148. db.dump_db(self.env.cr.dbname, destiny)
  149. backup = backup or destiny.name
  150. successful |= rec
  151. # Ensure a local backup exists if we are going to write it remotely
  152. sftp = self.filtered(lambda r: r.method == "sftp")
  153. if sftp:
  154. if backup:
  155. cached = open(backup)
  156. else:
  157. cached = tempfile.TemporaryFile()
  158. db.dump_db(self.env.cr.dbname, cached)
  159. with cached:
  160. for rec in sftp:
  161. with rec.backup_log():
  162. with rec.sftp_connection() as remote:
  163. # Directory must exist
  164. try:
  165. remote.makedirs(rec.folder)
  166. except pysftp.ConnectionException:
  167. pass
  168. # Copy cached backup to remote server
  169. with remote.open(
  170. os.path.join(rec.folder, filename),
  171. "wb") as destiny:
  172. shutil.copyfileobj(cached, destiny)
  173. successful |= rec
  174. # Remove old files for successful backups
  175. successful.cleanup()
  176. @api.model
  177. def action_backup_all(self):
  178. """Run all scheduled backups."""
  179. return self.search([]).action_backup()
  180. @api.multi
  181. @contextmanager
  182. def backup_log(self):
  183. """Log a backup result."""
  184. try:
  185. _logger.info("Starting database backup: %s", self.name)
  186. yield
  187. except:
  188. _logger.exception("Database backup failed: %s", self.name)
  189. escaped_tb = tools.html_escape(traceback.format_exc())
  190. self.message_post(
  191. "<p>%s</p><pre>%s</pre>" % (
  192. _("Database backup failed."),
  193. escaped_tb),
  194. subtype=self.env.ref("auto_backup.failure"))
  195. else:
  196. _logger.info("Database backup succeeded: %s", self.name)
  197. self.message_post(_("Database backup succeeded."))
  198. @api.multi
  199. def cleanup(self):
  200. """Clean up old backups."""
  201. now = datetime.now()
  202. for rec in self.filtered("days_to_keep"):
  203. with rec.cleanup_log():
  204. oldest = self.filename(now - timedelta(days=rec.days_to_keep))
  205. if rec.method == "local":
  206. for name in iglob(os.path.join(rec.folder,
  207. "*.dump.zip")):
  208. if name < oldest:
  209. os.unlink(name)
  210. elif rec.method == "sftp":
  211. with rec.sftp_connection() as remote:
  212. for name in remote.listdir(rec.folder):
  213. if name.endswith(".dump.zip") and name < oldest:
  214. remote.unlink(name)
  215. @api.multi
  216. @contextmanager
  217. def cleanup_log(self):
  218. """Log a possible cleanup failure."""
  219. try:
  220. _logger.info("Starting cleanup process after database backup: %s",
  221. self.name)
  222. yield
  223. except:
  224. _logger.exception("Cleanup of old database backups failed: %s")
  225. escaped_tb = tools.html_escape(traceback.format_exc())
  226. self.message_post(
  227. "<p>%s</p><pre>%s</pre>" % (
  228. _("Cleanup of old database backups failed."),
  229. escaped_tb),
  230. subtype=self.env.ref("auto_backup.failure"))
  231. else:
  232. _logger.info("Cleanup of old database backups succeeded: %s",
  233. self.name)
  234. @api.model
  235. def filename(self, when):
  236. """Generate a file name for a backup.
  237. :param datetime.datetime when:
  238. Use this datetime instead of :meth:`datetime.datetime.now`.
  239. """
  240. return "{:%Y_%m_%d_%H_%M_%S}.dump.zip".format(when)
  241. @api.multi
  242. def sftp_connection(self):
  243. """Return a new SFTP connection with found parameters."""
  244. params = {
  245. "host": self.sftp_host,
  246. "username": self.sftp_user,
  247. "port": self.sftp_port,
  248. }
  249. _logger.debug(
  250. "Trying to connect to sftp://%(username)s@%(host)s:%(port)d",
  251. extra=params)
  252. if self.sftp_private_key:
  253. params["private_key"] = self.stfpprivatekey
  254. if self.sftp_password:
  255. params["private_key_pass"] = self.sftp_password
  256. else:
  257. params["password"] = self.sftp_password
  258. return pysftp.Connection(**params)