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.

353 lines
11 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2011 Daniel Reis
  3. # Copyright 2016 LasLabs Inc.
  4. # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
  5. import logging
  6. import psycopg2
  7. from contextlib import contextmanager
  8. from odoo import _, api, fields, models, tools
  9. from ..exceptions import ConnectionFailedError, ConnectionSuccessError
  10. _logger = logging.getLogger(__name__)
  11. class BaseExternalDbsource(models.Model):
  12. """ It provides logic for connection to an external data source
  13. Classes implementing this interface must provide the following methods
  14. suffixed with the adapter type. See the method definitions and examples
  15. for more information:
  16. * ``connection_open_*``
  17. * ``connection_close_*``
  18. * ``execute_*``
  19. Optional methods for adapters to implement:
  20. * ``remote_browse_*``
  21. * ``remote_create_*``
  22. * ``remote_delete_*``
  23. * ``remote_search_*``
  24. * ``remote_update_*``
  25. """
  26. _name = "base.external.dbsource"
  27. _description = 'External Database Sources'
  28. CONNECTORS = [
  29. ('postgresql', 'PostgreSQL'),
  30. ]
  31. # This is appended to the conn string if pass declared but not detected.
  32. # Children should declare PWD_STRING_CONNECTOR (such as PWD_STRING_FBD)
  33. # to allow for override.
  34. PWD_STRING = 'PWD=%s;'
  35. name = fields.Char('Datasource name', required=True, size=64)
  36. conn_string = fields.Text('Connection string', help="""
  37. Sample connection strings:
  38. - Microsoft SQL Server:
  39. mssql+pymssql://username:%s@server:port/dbname?charset=utf8
  40. - MySQL: mysql://user:%s@server:port/dbname
  41. - ODBC: DRIVER={FreeTDS};SERVER=server.address;Database=mydb;UID=sa
  42. - ORACLE: username/%s@//server.address:port/instance
  43. - PostgreSQL:
  44. dbname='template1' user='dbuser' host='localhost' port='5432' \
  45. password=%s
  46. - SQLite: sqlite:///test.db
  47. - Elasticsearch: https://user:%s@localhost:9200
  48. """)
  49. conn_string_full = fields.Text(
  50. readonly=True,
  51. compute='_compute_conn_string_full',
  52. )
  53. password = fields.Char('Password', size=40)
  54. client_cert = fields.Text()
  55. client_key = fields.Text()
  56. ca_certs = fields.Char(
  57. help='Path to CA Certs file on server.',
  58. )
  59. connector = fields.Selection(
  60. CONNECTORS, 'Connector', required=True,
  61. help="If a connector is missing from the list, check the server "
  62. "log to confirm that the required components were detected.",
  63. )
  64. current_table = None
  65. @api.multi
  66. @api.depends('conn_string', 'password')
  67. def _compute_conn_string_full(self):
  68. for record in self:
  69. if record.password:
  70. if '%s' not in record.conn_string:
  71. pwd_string = getattr(
  72. record,
  73. 'PWD_STRING_%s' % record.connector.upper(),
  74. record.PWD_STRING,
  75. )
  76. record.conn_string += pwd_string
  77. record.conn_string_full = record.conn_string % record.password
  78. else:
  79. record.conn_string_full = record.conn_string
  80. # Interface
  81. @api.multi
  82. def change_table(self, name):
  83. """ Change the table that is used for CRUD operations """
  84. self.current_table = name
  85. @api.multi
  86. def connection_close(self, connection):
  87. """ It closes the connection to the data source.
  88. This method calls adapter method of this same name, suffixed with
  89. the adapter type.
  90. """
  91. method = self._get_adapter_method('connection_close')
  92. return method(connection)
  93. @api.multi
  94. @contextmanager
  95. def connection_open(self):
  96. """ It provides a context manager for the data source.
  97. This method calls adapter method of this same name, suffixed with
  98. the adapter type.
  99. """
  100. method = self._get_adapter_method('connection_open')
  101. try:
  102. connection = method()
  103. yield connection
  104. finally:
  105. try:
  106. self.connection_close(connection)
  107. except:
  108. _logger.exception('Connection close failure.')
  109. @api.multi
  110. def execute(
  111. self, query=None, execute_params=None, metadata=False, **kwargs
  112. ):
  113. """ Executes a query and returns a list of rows.
  114. "execute_params" can be a dict of values, that can be referenced
  115. in the SQL statement using "%(key)s" or, in the case of Oracle,
  116. ":key".
  117. Example:
  118. query = "SELECT * FROM mytable WHERE city = %(city)s AND
  119. date > %(dt)s"
  120. execute_params = {
  121. 'city': 'Lisbon',
  122. 'dt': datetime.datetime(2000, 12, 31),
  123. }
  124. If metadata=True, it will instead return a dict containing the
  125. rows list and the columns list, in the format:
  126. { 'cols': [ 'col_a', 'col_b', ...]
  127. , 'rows': [ (a0, b0, ...), (a1, b1, ...), ...] }
  128. """
  129. # Old API compatibility
  130. if not query:
  131. try:
  132. query = kwargs['sqlquery']
  133. except KeyError:
  134. raise TypeError(_('query is a required argument'))
  135. if not execute_params:
  136. try:
  137. execute_params = kwargs['sqlparams']
  138. except KeyError:
  139. pass
  140. method = self._get_adapter_method('execute')
  141. rows, cols = method(query, execute_params, metadata)
  142. if metadata:
  143. return {'cols': cols, 'rows': rows}
  144. else:
  145. return rows
  146. @api.multi
  147. def connection_test(self):
  148. """ It tests the connection
  149. Raises:
  150. ConnectionSuccessError: On connection success
  151. ConnectionFailedError: On connection failed
  152. """
  153. for obj in self:
  154. try:
  155. with self.connection_open():
  156. pass
  157. except Exception as e:
  158. raise ConnectionFailedError(_(
  159. "Connection test failed:\n"
  160. "Here is what we got instead:\n%s"
  161. ) % tools.ustr(e))
  162. raise ConnectionSuccessError(_(
  163. "Connection test succeeded:\n"
  164. "Everything seems properly set up!",
  165. ))
  166. @api.multi
  167. def remote_browse(self, record_ids, *args, **kwargs):
  168. """ It browses for and returns the records from remote by ID
  169. This method calls adapter method of this same name, suffixed with
  170. the adapter type.
  171. Args:
  172. record_ids: (list) List of remote IDs to browse.
  173. *args: Positional arguments to be passed to adapter method.
  174. **kwargs: Keyword arguments to be passed to adapter method.
  175. Returns:
  176. (iter) Iterator of record mappings that match the ID.
  177. """
  178. assert self.current_table
  179. method = self._get_adapter_method('remote_browse')
  180. return method(record_ids, *args, **kwargs)
  181. @api.multi
  182. def remote_create(self, vals, *args, **kwargs):
  183. """ It creates a record on the remote data source.
  184. This method calls adapter method of this same name, suffixed with
  185. the adapter type.
  186. Args:
  187. vals: (dict) Values to use for creation.
  188. *args: Positional arguments to be passed to adapter method.
  189. **kwargs: Keyword arguments to be passed to adapter method.
  190. Returns:
  191. (mapping) A mapping of the record that was created.
  192. """
  193. assert self.current_table
  194. method = self._get_adapter_method('remote_create')
  195. return method(vals, *args, **kwargs)
  196. @api.multi
  197. def remote_delete(self, record_ids, *args, **kwargs):
  198. """ It deletes records by ID on remote
  199. This method calls adapter method of this same name, suffixed with
  200. the adapter type.
  201. Args:
  202. record_ids: (list) List of remote IDs to delete.
  203. *args: Positional arguments to be passed to adapter method.
  204. **kwargs: Keyword arguments to be passed to adapter method.
  205. Returns:
  206. (iter) Iterator of bools indicating delete status.
  207. """
  208. assert self.current_table
  209. method = self._get_adapter_method('remote_delete')
  210. return method(record_ids, *args, **kwargs)
  211. @api.multi
  212. def remote_search(self, query, *args, **kwargs):
  213. """ It searches the remote for the query.
  214. This method calls adapter method of this same name, suffixed with
  215. the adapter type.
  216. Args:
  217. query: (mixed) Query domain as required by the adapter.
  218. *args: Positional arguments to be passed to adapter method.
  219. **kwargs: Keyword arguments to be passed to adapter method.
  220. Returns:
  221. (iter) Iterator of record mappings that match query.
  222. """
  223. assert self.current_table
  224. method = self._get_adapter_method('remote_search')
  225. return method(query, *args, **kwargs)
  226. @api.multi
  227. def remote_update(self, record_ids, vals, *args, **kwargs):
  228. """ It updates the remote records with the vals
  229. This method calls adapter method of this same name, suffixed with
  230. the adapter type.
  231. Args:
  232. record_ids: (list) List of remote IDs to delete.
  233. *args: Positional arguments to be passed to adapter method.
  234. **kwargs: Keyword arguments to be passed to adapter method.
  235. Returns:
  236. (iter) Iterator of record mappings that were updated.
  237. """
  238. assert self.current_table
  239. method = self._get_adapter_method('remote_update')
  240. return method(record_ids, vals, *args, **kwargs)
  241. # Adapters
  242. def connection_close_postgresql(self, connection):
  243. return connection.close()
  244. def connection_open_postgresql(self):
  245. return psycopg2.connect(self.conn_string)
  246. def execute_postgresql(self, query, params, metadata):
  247. return self._execute_generic(query, params, metadata)
  248. def _execute_generic(self, query, params, metadata):
  249. with self.connection_open() as connection:
  250. cur = connection.cursor()
  251. cur.execute(query, params)
  252. cols = []
  253. if metadata:
  254. cols = [d[0] for d in cur.description]
  255. rows = cur.fetchall()
  256. return rows, cols
  257. # Compatibility & Private
  258. @api.multi
  259. def conn_open(self):
  260. """ It opens and returns a connection to the remote data source.
  261. This method calls adapter method of this same name, suffixed with
  262. the adapter type.
  263. Deprecate:
  264. This method has been replaced with ``connection_open``.
  265. """
  266. with self.connection_open() as connection:
  267. return connection
  268. def _get_adapter_method(self, method_prefix):
  269. """ It returns the connector adapter method for ``method_prefix``.
  270. Args:
  271. method_prefix: (str) Prefix of adapter method (such as
  272. ``connection_open``).
  273. Raises:
  274. NotImplementedError: When the method is not found
  275. Returns:
  276. (instancemethod)
  277. """
  278. self.ensure_one()
  279. method = '%s_%s' % (method_prefix, self.connector)
  280. try:
  281. return getattr(self, method)
  282. except AttributeError:
  283. raise NotImplementedError(_(
  284. '"%s" method not found, check that all assets are installed '
  285. 'for the %s connector type.'
  286. )) % (
  287. method, self.connector,
  288. )