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.

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