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.

376 lines
14 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 xmlrpclib
  22. import socket
  23. import os
  24. import time
  25. import datetime
  26. import re
  27. from openerp import models, fields, api, _
  28. from openerp.exceptions import except_orm, Warning as UserError
  29. from openerp import tools
  30. from openerp.service import db
  31. import logging
  32. _logger = logging.getLogger(__name__)
  33. try:
  34. import pysftp
  35. except ImportError:
  36. _logger.debug('Can not import pysftp')
  37. def execute(connector, method, *args):
  38. res = False
  39. try:
  40. res = getattr(connector, method)(*args)
  41. except socket.error as e:
  42. raise e
  43. return res
  44. class DbBackup(models.Model):
  45. _name = 'db.backup'
  46. @api.model
  47. def _get_db_name(self):
  48. return self.env.cr.dbname
  49. name = fields.Char(
  50. string='Database', size=100, required=True,
  51. default=_get_db_name,
  52. help='Database you want to schedule backups for'
  53. )
  54. bkp_dir = fields.Char(
  55. string='Backup Directory', size=100,
  56. default='/odoo/backups',
  57. help='Absolute path for storing the backups',
  58. required=True
  59. )
  60. autoremove = fields.Boolean(
  61. string='Auto. Remove Backups',
  62. help=(
  63. "If you check this option you can choose to "
  64. "automaticly remove the backup after xx days"
  65. )
  66. )
  67. daystokeep = fields.Integer(
  68. string='Remove after x days',
  69. default=30,
  70. help=(
  71. "Choose after how many days the backup should be "
  72. "deleted. For example:\nIf you fill in 5 the backups "
  73. "will be removed after 5 days."
  74. ), required=True
  75. )
  76. sftpwrite = fields.Boolean(
  77. string='Write to external server with sftp',
  78. help=(
  79. "If you check this option you can specify the details "
  80. "needed to write to a remote server with SFTP."
  81. )
  82. )
  83. sftppath = fields.Char(
  84. string='Path external server',
  85. help=(
  86. "The location to the folder where the dumps should be "
  87. "written to. For example /odoo/backups/.\nFiles will then"
  88. " be written to /odoo/backups/ on your remote server."
  89. )
  90. )
  91. sftpip = fields.Char(
  92. string='IP Address SFTP Server',
  93. help=(
  94. "The IP address from your remote"
  95. " server. For example 192.168.0.1"
  96. )
  97. )
  98. sftpport = fields.Integer(
  99. string="SFTP Port",
  100. default=22,
  101. help="The port on the FTP server that accepts SSH/SFTP calls."
  102. )
  103. sftpusername = fields.Char(
  104. string='Username SFTP Server',
  105. help=(
  106. "The username where the SFTP connection "
  107. "should be made with. This is the user on the external server."
  108. )
  109. )
  110. sftppassword = fields.Char(
  111. string='Password User SFTP Server',
  112. help=(
  113. "The password from the user where the SFTP connection "
  114. "should be made with. This is the password from the user"
  115. " on the external server."
  116. )
  117. )
  118. daystokeepsftp = fields.Integer(
  119. string='Remove SFTP after x days',
  120. default=30,
  121. help=(
  122. "Choose after how many days the backup should be deleted "
  123. "from the FTP server. For example:\nIf you fill in 5 the "
  124. "backups will be removed after 5 days from the FTP server."
  125. )
  126. )
  127. sendmailsftpfail = fields.Boolean(
  128. string='Auto. E-mail on backup fail',
  129. help=(
  130. "If you check this option you can choose to automaticly"
  131. " get e-mailed when the backup to the external server failed."
  132. )
  133. )
  134. emailtonotify = fields.Char(
  135. string='E-mail to notify',
  136. help=(
  137. "Fill in the e-mail where you want to be"
  138. " notified that the backup failed on the FTP."
  139. )
  140. )
  141. lasterrorlog = fields.Text(
  142. string='E-mail to notify',
  143. help=(
  144. "Fill in the e-mail where you want to be"
  145. " notified that the backup failed on the FTP."
  146. )
  147. )
  148. @api.multi
  149. def _check_db_exist(self):
  150. for rec in self:
  151. db_list = self.get_db_list(rec.host, rec.port, rec.securehost)
  152. if rec.name in db_list:
  153. return True
  154. return False
  155. _constraints = [
  156. (
  157. _check_db_exist,
  158. _('Error ,No such database exists'), ['name']
  159. )
  160. ]
  161. @api.multi
  162. def test_sftp_connection(self):
  163. confs = self.search([])
  164. # Check if there is a success or fail and write messages
  165. messageTitle = ""
  166. messageContent = ""
  167. for rec in confs:
  168. try:
  169. conn_success = True
  170. ipHost = rec.sftpip
  171. portHost = rec.sftpport
  172. usernameLogin = rec.sftpusername
  173. passwordLogin = rec.sftppassword
  174. # Connect with external server over SFTP, so we know sure that
  175. # everything works.
  176. srv = pysftp.Connection(host=ipHost, username=usernameLogin,
  177. password=passwordLogin, port=portHost)
  178. srv.close()
  179. # We have a success.
  180. messageTitle = _("Connection Test Succeeded!")
  181. messageContent = _(
  182. "Everything seems properly set up for FTP back-ups!")
  183. except Exception as e:
  184. conn_success = False
  185. messageTitle = _("Connection Test Failed!")
  186. if len(rec.sftpip) < 8:
  187. messageContent += _(
  188. "\nYour IP address seems to be too short.\n")
  189. messageContent += _("Here is what we got instead:\n")
  190. if not conn_success:
  191. raise except_orm(
  192. _(messageTitle), _(
  193. messageContent + "%s") %
  194. tools.ustr(e))
  195. else:
  196. raise UserError(_(messageTitle), _(messageContent))
  197. @api.model
  198. def schedule_backup(self):
  199. for rec in self.search([]):
  200. if not os.path.isdir(rec.bkp_dir):
  201. os.makedirs(rec.bkp_dir)
  202. # Create name for dumpfile.
  203. bkp_file = '%s_%s.dump.zip' % (
  204. time.strftime('%d_%m_%Y_%H_%M_%S'),
  205. rec.name)
  206. file_path = os.path.join(rec.bkp_dir, bkp_file)
  207. fbk = open(file_path, 'wb')
  208. db.dump_db(rec.name, fbk)
  209. fbk.close()
  210. # Check if user wants to write to SFTP or not.
  211. if rec.sftpwrite is True:
  212. try:
  213. # Store all values in variables
  214. dir = rec.bkp_dir
  215. pathToWriteTo = rec.sftppath
  216. ipHost = rec.sftpip
  217. portHost = rec.sftpport
  218. usernameLogin = rec.sftpusername
  219. passwordLogin = rec.sftppassword
  220. # Connect with external server over SFTP
  221. srv = pysftp.Connection(
  222. host=ipHost,
  223. username=usernameLogin,
  224. password=passwordLogin,
  225. port=portHost)
  226. # Move to the correct directory on external server. If the
  227. # user made a typo in his path with multiple slashes
  228. # (/odoo//backups/) it will be fixed by this regex.
  229. pathToWriteTo = re.sub('/+', '/', pathToWriteTo)
  230. _logger.debug(
  231. 'Start to copy files..'
  232. )
  233. try:
  234. srv.chdir(pathToWriteTo)
  235. except IOError:
  236. # Create directory and subdirs if they do not exist.
  237. currentDir = ''
  238. for dirElement in pathToWriteTo.split('/'):
  239. currentDir += dirElement + '/'
  240. try:
  241. srv.chdir(currentDir)
  242. except:
  243. _logger.info(
  244. _(
  245. '(Part of the) path didn\'t exist. '
  246. 'Creating it now at %s'
  247. ) % currentDir
  248. )
  249. # Make directory and then navigate into it
  250. srv.mkdir(currentDir, mode=777)
  251. srv.chdir(currentDir)
  252. pass
  253. srv.chdir(pathToWriteTo)
  254. # Loop over all files in the directory.
  255. for f in os.listdir(dir):
  256. fullpath = os.path.join(dir, f)
  257. if os.path.isfile(fullpath):
  258. srv.put(fullpath)
  259. # Navigate in to the correct folder.
  260. srv.chdir(pathToWriteTo)
  261. # Loop over all files in the directory from the back-ups.
  262. # We will check the creation date of every back-up.
  263. for file in srv.listdir(pathToWriteTo):
  264. # Get the full path
  265. fullpath = os.path.join(pathToWriteTo, file)
  266. if srv.isfile(fullpath) and ".dump.zip" in file:
  267. # Get the timestamp from the file on the external
  268. # server
  269. timestamp = srv.stat(fullpath).st_atime
  270. createtime = (
  271. datetime.datetime.fromtimestamp(timestamp)
  272. )
  273. now = datetime.datetime.now()
  274. delta = now - createtime
  275. # If the file is older than the daystokeepsftp (the
  276. # days to keep that the user filled in on the Odoo
  277. # form it will be removed.
  278. if (
  279. rec.daystokeepsftp > 0 and
  280. delta.days >= rec.daystokeepsftp
  281. ):
  282. # Only delete files, no directories!
  283. srv.unlink(file)
  284. # Close the SFTP session.
  285. srv.close()
  286. except Exception as e:
  287. _logger.debug(
  288. 'Exception We couldn\'t back '
  289. 'up to the FTP server..'
  290. )
  291. # At this point the SFTP backup failed.
  292. # We will now check if the user wants
  293. # an e-mail notification about this.
  294. if rec.sendmailsftpfail:
  295. self.send_notification(rec, e)
  296. # Remove all old files (on local server)
  297. # in case this is configured..
  298. if rec.autoremove is True:
  299. try:
  300. self.remove_folder(rec)
  301. except Exception as e:
  302. _logger.debug(
  303. 'Exception when try to remove file'
  304. )
  305. return True
  306. def send_notification(self, rec, e):
  307. try:
  308. ir_mail_server = self.env['ir.mail_server']
  309. message = (
  310. "Dear,\n\nThe backup for the server %s"
  311. " (IP: %s) failed.Please check"
  312. " the following details:\n\n"
  313. "IP address SFTP server: %s \nUsername: %s"
  314. "\nPassword: %s"
  315. "\n\nError details: %s \n\nWith kind regards"
  316. ) % (
  317. rec.host, rec.sftpip, rec.sftpip,
  318. rec.sftpusername, rec.sftppassword,
  319. tools.ustr(e)
  320. )
  321. msg = ir_mail_server.build_email(
  322. "auto_backup@%s.com" % rec.name,
  323. [rec.emailtonotify],
  324. "Backup from %s ( %s ) failed" % (
  325. rec.host, rec.sftpip),
  326. message)
  327. ir_mail_server.send_email(msg)
  328. except Exception as e:
  329. _logger.debug(
  330. 'Exception %s' % tools.ustr(e)
  331. )
  332. # This is done after the SFTP writing to prevent unusual behaviour:
  333. # If the user would set local back-ups to be kept 0 days and the SFTP
  334. # to keep backups xx days there wouldn't be any new back-ups added
  335. # to the SFTP.
  336. # If we'd remove the dump files before they're writen to the SFTP
  337. # there willbe nothing to write. Meaning that if an user doesn't want
  338. # to keep back-ups locally and only wants them on the SFTP
  339. # (NAS for example) there wouldn't be any writing to the
  340. # remote server if this if statement was before the SFTP write method
  341. # right above this comment.
  342. def remove_folder(self, rec):
  343. dir = rec.bkp_dir
  344. # Loop over all files in the directory.
  345. for f in os.listdir(dir):
  346. fullpath = os.path.join(dir, f)
  347. if os.path.isfile(fullpath) and ".dump.zip" in f:
  348. timestamp = os.stat(fullpath).st_ctime
  349. createtime = (
  350. datetime.datetime.fromtimestamp(timestamp)
  351. )
  352. now = datetime.datetime.now()
  353. delta = now - createtime
  354. if delta.days >= rec.daystokeep:
  355. _logger.debug(
  356. 'Remove local file...'
  357. )
  358. os.remove(fullpath)