odoo_instance.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. # -*- coding: utf-8 -*-
  2. from openerp import models, fields, api
  3. from openerp.tools import email_send
  4. from openerp.exceptions import Warning, ValidationError
  5. from jinja2 import Environment as JinjaEnv, PackageLoader as JinjaLoader
  6. from ..utils.docker_api import (
  7. get_all_external_ports,
  8. run_command,
  9. copy_in,
  10. run_container,
  11. get_internal_ip,
  12. get_status,
  13. stop_container,
  14. start_container,
  15. restart_container
  16. )
  17. from ..utils.command import execute, list_files_and_folders
  18. from random import randint
  19. from git.util import join_path
  20. from git.repo.base import Repo
  21. from git.exc import GitCommandError
  22. import unicodedata
  23. import stringcase
  24. import time
  25. import os
  26. class OdooInstance(models.Model):
  27. _name = 'odoo.instance'
  28. CONTAINER_STATUS = [
  29. ('draft', 'Por activar'),
  30. ('activated', 'Activado'),
  31. ('disapproved', 'No aprobado'),
  32. ('suspended', 'Suspendido'),
  33. ('destroyed', 'Eliminado')
  34. ]
  35. DEFAULT_DIRS = [
  36. 'config',
  37. 'custom-addons',
  38. 'files'
  39. ]
  40. # 'config: /etc/odoo, custom-addons: /mnt/extra-addons, files: /var/lib/odoo'
  41. def _snakeike_name(self, name):
  42. try:
  43. name = unicodedata.normalize('NFKD', name)
  44. name = name.encode('ASCII', 'ignore')
  45. except TypeError:
  46. pass
  47. name = stringcase.trimcase(name)
  48. name = stringcase.lowercase(name)
  49. name = stringcase.snakecase(name)
  50. return name
  51. def _check_port_availability(self, port):
  52. return port not in get_all_external_ports()
  53. def _check_name_availability(self, name):
  54. if len(name) == 0:
  55. return False
  56. root_path = self.env['odoo.management.config'].get_default_settings([]).get('odoo_root_path')
  57. if len(root_path) == 0:
  58. raise Warning('La ruta por defecto para los sistemas no está definido')
  59. full_path = os.path.join(root_path, name)
  60. return not os.path.exists(full_path)
  61. def _randomize_port(self):
  62. ports = self.env['odoo.management.config'].get_default_settings([]).get('odoo_ports_range')
  63. ports = filter(lambda x: x != '', ports.split(','))
  64. if len(ports) == 0:
  65. ports = [10000, 20000]
  66. # raise Warning('El rango de puertos para los sistemas no está definido')
  67. port = 0
  68. while not self._check_port_availability(port):
  69. port = randint(ports[0], ports[1])
  70. time.sleep(0.05)
  71. return port
  72. def _make_default_dirs(self, name):
  73. root_path = self.env['ir.values'].get_default(self._name, 'odoo_root_path')
  74. for d in self.DEFAULT_DIRS:
  75. full_path = os.path.join(root_path, name, d.strip())
  76. os.makedirs(full_path)
  77. def _delete_dirs(self, name):
  78. root_path = self.env['ir.values'].get_default(self._name, 'odoo_root_path')
  79. full_path = os.path.join(root_path, name)
  80. execute(['rm', '-Rf', full_path])
  81. def _make_config_file(self, name):
  82. root_path = self.env['ir.values'].get_default(self._name, 'odoo_root_path')
  83. config_path = os.path.join(root_path, name, 'config')
  84. if not os.path.exists(config_path):
  85. raise Warning('No se creó el directorio para el archivo de configuración')
  86. env = JinjaEnv(loader=JinjaLoader('assets', 'templates'))
  87. tmpl = env.get_template('openerp-server.j2')
  88. tmpl_rendered = tmpl.stream({
  89. 'admin_password': 'admin',
  90. 'db_host': '',
  91. 'db_port': '',
  92. 'db_name': '',
  93. 'db_user': '',
  94. 'db_password': ''
  95. })
  96. tmpl_rendered.dump(os.path.join(config_path, 'openerp-config.conf'))
  97. def _create_database(self, name):
  98. odoo_db = self.env['ir.values'].get_default(self._name, 'odoo_db_link')
  99. cmd = 'createdb -U %s %s' % ('odoo', name)
  100. output = run_command(odoo_db, cmd)
  101. return output
  102. def _drop_database(self, name):
  103. odoo_db = self.env['ir.values'].get_default(self._name, 'odoo_db_link')
  104. terminate_cmd = "psql -U %s select pg_terminate_backend(pid) from pg_stats where datname = '%s'" % ('odoo', name)
  105. run_command(odoo_db, terminate_cmd)
  106. drop_cmd = 'drop database %s' % name
  107. run_command(odoo_db, drop_cmd)
  108. def _copy_database_seed(self):
  109. odoo_db = self.env['ir.values'].get_default(self._name, 'odoo_db_link')
  110. backup_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'files', 'odoo.tar')
  111. output = copy_in(odoo_db, '/tmp', backup_path)
  112. return output
  113. def _restore_database(self, name):
  114. odoo_db = self.env['ir.values'].get_default(self._name, 'odoo_db_link')
  115. cmd = 'psql -U %s -d %s -f %s' % (odoo_db, name, '/tmp/odoo.sql')
  116. output = run_command(odoo_db, cmd)
  117. return output
  118. def _remove_database_seed(self):
  119. odoo_db = self.env['ir.values'].get_default(self._name, 'odoo_db_link')
  120. cmd = 'rm -f %s' % '/tmp/odoo.sql'
  121. output = run_command(odoo_db, cmd)
  122. return output
  123. def _create_container(self, name, ports=[]):
  124. ir_values = self.env['ir.values']
  125. root_path = ir_values.get_default(self._name, 'odoo_root_path')
  126. odoo_docker_image = ir_values.get_default(self._name, 'odoo_docker_image')
  127. odoo_network = ir_values.get_default(self._name, 'odoo_network')
  128. ports = dict(map(lambda p: ('%d/tcp' % 8069, p), ports))
  129. volumes = dict()
  130. run_container(
  131. odoo_docker_image,
  132. name,
  133. ports,
  134. volumes,
  135. odoo_network,
  136. '150m',
  137. '150m'
  138. )
  139. time.sleep(3)
  140. return True
  141. def _apply_permissions(self, name):
  142. root_path = self.env['ir.values'].get_default(self._name, 'odoo_root_path')
  143. full_path = os.path.join(root_path, name)
  144. execute(['chmod', '-Rf', '777'], full_path)
  145. return True
  146. def _get_internal_ip(self, name):
  147. return get_internal_ip(name)
  148. def _clone_repositories(self, names=[], path=None, branch='master'):
  149. if len(names) == 0 or not path:
  150. return False
  151. git_path = self.env['ir.values'].get_default(self._name, 'modules_git_path')
  152. paths = list_files_and_folders(git_path)
  153. if len(git_path) == 0:
  154. raise Warning('La ruta por defecto del repositorio no está definido')
  155. for path in paths:
  156. for name in names:
  157. repo_path = '%s.git' % join_path(git_path, path, name)
  158. if not os.path.exists(repo_path):
  159. continue
  160. try:
  161. repo = Repo(repo_path)
  162. repo.clone(join_path(path, name), branch=branch)
  163. repo.close()
  164. except GitCommandError:
  165. pass
  166. return True
  167. def _clone_repository(self, name, path):
  168. result = self._clone_repositories([name], path)
  169. return result
  170. def _copy_modules(self, name, modules=[]):
  171. root_path = self.env['ir.values'].get_default(self._name, 'odoo_root_path')
  172. if not os.path.exists(root_path):
  173. raise Warning('El directorio del sistema no existe')
  174. container_is_running = get_status(name)
  175. if container_is_running:
  176. stopped = stop_container(name)
  177. if not stopped:
  178. raise Warning('No se pudo parar el contenedor')
  179. for module in modules:
  180. module_path = os.path.join(root_path, 'custom-addons', module)
  181. if os.path.exists(module_path):
  182. execute(['rm', '-Rf', module_path])
  183. addons_path = os.path.join(root_path, 'custom-addons')
  184. self._clone_repository(module, addons_path)
  185. git_data_path = os.path.join(module, '.git')
  186. if os.path.exists(git_data_path):
  187. execute(['rm', '-Rf', git_data_path])
  188. try:
  189. execute(['chmod', '-Rf', '777', module_path])
  190. except Exception:
  191. pass
  192. if container_is_running:
  193. started = start_container(name)
  194. if not started:
  195. raise Warning('No se pudo arrancar el contenedor')
  196. def _copy_module(self, name, module):
  197. self._copy_modules(name, [module])
  198. # def _handle_container_event(self, e):
  199. # event_type = e.get('Type')
  200. # event_action = e.get('Action')
  201. # container_id = e.get('Actor').get('ID')
  202. #
  203. # print(event_type)
  204. # print(event_action)
  205. # print(container_id)
  206. #
  207. # def _register_hook(self, cr):
  208. # run_watchdog(self._handle_container_event)
  209. # super(OdooInstance, self)._register_hook(cr)
  210. name = fields.Char(string='Nombre', size=50)
  211. normalized_name = fields.Char(string='Nombre normalizado', compute='_normalize_name', size=50)
  212. logo = fields.Binary(string='Logo')
  213. internal_ip = fields.Char(string='IP interno', size=15)
  214. internal_port = fields.Integer(string='Puerto interno')
  215. external_ip = fields.Char(string='IP externo', size=15)
  216. external_port = fields.Integer(string='Puerto externo')
  217. expose_ip = fields.Boolean(string='Exponer IP', default=True)
  218. state = fields.Selection(string='Estado', selection=CONTAINER_STATUS, default='draft')
  219. demo = fields.Boolean(string='Es un demo?', default=False)
  220. domain = fields.Char(string='Dominio', size=100)
  221. running = fields.Boolean(string='Está online?', default=False)
  222. payment_plan_id = fields.Many2one(string='Plan de pago', comodel_name='payment.plan', required=True)
  223. def _send_email(self, ):
  224. email_send()
  225. @api.model
  226. def create(self, values):
  227. snaked_name = self._snakeike_name(values.get('name'))
  228. name_is_available = self._check_name_availability(snaked_name)
  229. if not name_is_available:
  230. raise ValidationError('El nombre ya está siendo usado por otro sistema')
  231. return super(OdooInstance, self).create(values)
  232. @api.one
  233. @api.onchange('name')
  234. def _onchange_name(self):
  235. if self.name:
  236. self.name = self.name.title()
  237. @api.one
  238. @api.depends('name')
  239. def _normalize_name(self):
  240. self.normalized_name = self._snakeike_name(self.name)
  241. @api.one
  242. def action_activate(self):
  243. if self.state not in ('draft', 'suspended'):
  244. raise Warning('No se puede activar un sistema ya activo')
  245. # 1. Check name
  246. name_is_available = self._check_name_availability(self.normalized_name)
  247. if not name_is_available:
  248. raise Warning('El nombre ya está siendo usado por otro sistema')
  249. # 2. Get a port
  250. port_to_use = self._randomize_port()
  251. # 3. Create dirs
  252. make_ok = self.make_default_dirs(self.normalized_name)
  253. if not make_ok:
  254. raise Warning('No se pudo crear la estructura de directorios')
  255. # 4. Create configuration file
  256. config_ok = self.make_config_file(self.normalized_name)
  257. if not config_ok:
  258. raise Warning('No se pudo crear el archivo de configuración')
  259. # 5. Create database
  260. db_ok = self.create_database(self.normalized_name)
  261. if not db_ok:
  262. raise Warning('No se pudo crear la base de datos')
  263. # 6. Copy database seed
  264. seed_copied = self.copy_database_seed()
  265. if not seed_copied:
  266. raise Warning('No se pudo copiar la estructura inicial de la base de datos')
  267. # 7. Restore database schema
  268. restored = self.restore_database(self.normalized_name)
  269. if not restored:
  270. raise Warning('No se pudo reestablecer la copia de base de datos')
  271. # 8. Remove database seed
  272. seed_removed = self.remove_database_seed()
  273. if not seed_removed:
  274. raise Warning('No se pudo remover la copia de base de datos')
  275. # 9. Create odoo container
  276. odoo_created = self.create_container(self.normalized_name, [port_to_use])
  277. if not odoo_created:
  278. raise Warning('No se pudo crear el contenedor')
  279. # 10. Apply permissions
  280. permissions_applied = self.apply_permissions(self.normalized_name)
  281. if not permissions_applied:
  282. raise Warning('No se pudo aplicar los permisos correspondientes')
  283. # 11. Get internal ip
  284. internal_ip = self.get_internal_ip(self.normalized_name)
  285. # 12. Send email
  286. # TODO
  287. print(internal_ip)
  288. self.state = 'activated'
  289. @api.one
  290. def action_disapprove(self):
  291. if self.state != 'draft':
  292. raise Warning('No se puede desaprobar un sistema ya activo')
  293. self.state = 'disapproved'
  294. @api.one
  295. def action_suspend(self):
  296. if self.state != 'activated':
  297. raise Warning('No se puede suspender un sistema no activo')
  298. # 1. Get container status and stop it
  299. container_is_running = get_status(self.normalized_name)
  300. if container_is_running:
  301. stopped = stop_container(self.normalized_name)
  302. if not stopped:
  303. raise Warning('No se pudo parar el contenedor %s' % self.normalized_name)
  304. self.state = 'suspended'
  305. self.running = False
  306. @api.one
  307. def copy(self):
  308. raise Warning('Atención', 'Está prohibido duplicar una instancia. Por favor, cree uno nuevo')
  309. @api.one
  310. def action_destroy(self):
  311. if self.state == 'destroyed':
  312. raise Warning('No se puede eliminar un sistema ya eliminado')
  313. # 1. Get container status and stop it if necessary
  314. container_is_running = get_status(self.normalized_name)
  315. if container_is_running:
  316. stopped = stop_container(self.normalized_name)
  317. if not stopped:
  318. raise Warning('Atención', 'No se pudo parar el contenedor %s' % self.normalized_name)
  319. # 2. Delete dirs
  320. self._delete_dirs(self.normalized_name)
  321. # 3. Drop database
  322. self._drop_database(self.normalized_name)
  323. self.state = 'destroyed'
  324. self.running = False
  325. @api.one
  326. def action_start(self):
  327. if self.running:
  328. raise Warning('Atención', 'No se puede arrancar una instancia que ya está arrancada')
  329. # 1. Get container status
  330. container_is_running = get_status(self.normalized_name)
  331. if container_is_running:
  332. raise Warning('Atención', 'No se puede arrancar una instancia que ya está arrancada')
  333. # 2. Start container
  334. started = start_container(self.normalized_name)
  335. if not started:
  336. raise Warning('Atención', 'No se pudo arrancar el contenedor')
  337. self.running = True
  338. @api.one
  339. def action_restart(self):
  340. if not self.running:
  341. raise Warning('Atención', 'No se puede parar y arrancar una instancia que ya está parada')
  342. # 1. Get container status
  343. container_is_running = get_status(self.normalized_name)
  344. if not container_is_running:
  345. raise Warning('Atención', 'No se puede parar y arrancar una instancia que ya está parada')
  346. # 2. Restart container
  347. restarted = restart_container(self.normalized_name)
  348. if not restarted:
  349. raise Warning('Atención', 'No se pudo reiniciar el contenedor')
  350. self.running = True
  351. @api.model
  352. def check_status(self):
  353. print('croned')