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.

411 lines
16 KiB

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