| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | |
| 3 | ## |
| tierno | 9202102 | 2018-09-12 16:29:23 +0200 | [diff] [blame] | 4 | # Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U. |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 5 | # This file is part of openmano |
| 6 | # All Rights Reserved. |
| 7 | # |
| 8 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 9 | # not use this file except in compliance with the License. You may obtain |
| 10 | # a copy of the License at |
| 11 | # |
| 12 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 13 | # |
| 14 | # Unless required by applicable law or agreed to in writing, software |
| 15 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 16 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 17 | # License for the specific language governing permissions and limitations |
| 18 | # under the License. |
| 19 | # |
| 20 | # For those usages not covered by the Apache License, Version 2.0 please |
| 21 | # contact with: nfvlabs@tid.es |
| 22 | ## |
| 23 | |
| 24 | ''' |
| 25 | Base class for openmano database manipulation |
| 26 | ''' |
| 27 | __author__="Alfonso Tierno" |
| 28 | __date__ ="$4-Apr-2016 10:05:01$" |
| 29 | |
| 30 | import MySQLdb as mdb |
| 31 | import uuid as myUuid |
| 32 | import utils as af |
| 33 | import json |
| 34 | #import yaml |
| 35 | import time |
| 36 | import logging |
| 37 | import datetime |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 38 | from contextlib import contextmanager |
| 39 | from functools import wraps, partial |
| 40 | from threading import Lock |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 41 | from jsonschema import validate as js_v, exceptions as js_e |
| 42 | |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 43 | from .http_tools import errors as httperrors |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 44 | from .utils import Attempt, get_arg, inject_args |
| 45 | |
| 46 | |
| 47 | RECOVERY_TIME = 3 |
| 48 | |
| 49 | _ATTEMPT = Attempt() |
| 50 | |
| 51 | |
| 52 | def with_transaction(fn=None, cursor=None): |
| 53 | """Decorator that can be used together with instances of the ``db_base`` |
| 54 | class, to perform database actions wrapped in a commit/rollback fashion |
| 55 | |
| 56 | This decorator basically executes the function inside the context object |
| 57 | given by the ``transaction`` method in ``db_base`` |
| 58 | |
| 59 | Arguments: |
| 60 | cursor: [Optional] cursor class |
| 61 | """ |
| 62 | if fn is None: # Allows calling the decorator directly or with parameters |
| 63 | return partial(with_transaction, cursor=cursor) |
| 64 | |
| 65 | @wraps(fn) |
| 66 | def _wrapper(self, *args, **kwargs): |
| 67 | cursor_type = None |
| 68 | if cursor == 'dict': |
| 69 | # MySQLdB define the "cursors" module attribute lazily, |
| 70 | # so we have to defer references to mdb.cursors.DictCursor |
| 71 | cursor_type = mdb.cursors.DictCursor |
| 72 | |
| 73 | with self.transaction(cursor_type): |
| 74 | return fn(self, *args, **kwargs) |
| 75 | |
| 76 | return _wrapper |
| 77 | |
| 78 | |
| 79 | def retry(fn=None, max_attempts=Attempt.MAX, **info): |
| 80 | """Decorator that can be used together with instances of the ``db_base`` |
| 81 | class, to replay a method again after a unexpected error. |
| 82 | |
| 83 | The function being decorated needs to either be a method of ``db_base`` |
| 84 | subclasses or accept an ``db_base`` instance as the first parameter. |
| 85 | |
| 86 | All the extra keyword arguments will be passed to the ``_format_error`` |
| 87 | method |
| 88 | """ |
| 89 | if fn is None: # Allows calling the decorator directly or with parameters |
| 90 | return partial(retry, max_attempts=max_attempts, **info) |
| 91 | |
| 92 | @wraps(fn) |
| 93 | def _wrapper(*args, **kwargs): |
| 94 | self = args[0] |
| 95 | info.setdefault('table', get_arg('table', fn, args, kwargs)) |
| 96 | attempt = Attempt(max_attempts=max_attempts, info=info) |
| 97 | while attempt.countdown >= 0: |
| 98 | try: |
| 99 | return inject_args(fn, attempt=attempt)(*args, **kwargs) |
| 100 | except (mdb.Error, AttributeError) as ex: |
| 101 | self.logger.debug("Attempt #%d", attempt.number) |
| 102 | try: |
| 103 | # The format error will throw exceptions, however it can |
| 104 | # tolerate a certain amount of retries if it judges that |
| 105 | # the error can be solved with retrying |
| 106 | self._format_error(ex, attempt.countdown, **attempt.info) |
| 107 | # Anyway, unexpected/unknown errors can still be retried |
| 108 | except db_base_Exception as db_ex: |
| 109 | if (attempt.countdown < 0 or db_ex.http_code != |
| 110 | httperrors.Internal_Server_Error): |
| 111 | raise |
| 112 | |
| 113 | attempt.count += 1 |
| 114 | |
| 115 | return _wrapper |
| 116 | |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 117 | |
| 118 | def _check_valid_uuid(uuid): |
| 119 | id_schema = {"type" : "string", "pattern": "^[a-fA-F0-9]{8}(-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$"} |
| 120 | id_schema2 = {"type" : "string", "pattern": "^[a-fA-F0-9]{32}$"} |
| 121 | try: |
| 122 | js_v(uuid, id_schema) |
| 123 | return True |
| 124 | except js_e.ValidationError: |
| 125 | try: |
| 126 | js_v(uuid, id_schema2) |
| 127 | return True |
| 128 | except js_e.ValidationError: |
| 129 | return False |
| 130 | return False |
| 131 | |
| 132 | def _convert_datetime2str(var): |
| 133 | '''Converts a datetime variable to a string with the format '%Y-%m-%dT%H:%i:%s' |
| 134 | It enters recursively in the dict var finding this kind of variables |
| 135 | ''' |
| 136 | if type(var) is dict: |
| 137 | for k,v in var.items(): |
| 138 | if type(v) is datetime.datetime: |
| 139 | var[k]= v.strftime('%Y-%m-%dT%H:%M:%S') |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 140 | elif type(v) is dict or type(v) is list or type(v) is tuple: |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 141 | _convert_datetime2str(v) |
| 142 | if len(var) == 0: return True |
| 143 | elif type(var) is list or type(var) is tuple: |
| 144 | for v in var: |
| 145 | _convert_datetime2str(v) |
| 146 | |
| tierno | 44528e4 | 2016-10-11 12:06:25 +0000 | [diff] [blame] | 147 | def _convert_bandwidth(data, reverse=False, logger=None): |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 148 | '''Check the field bandwidth recursivelly and when found, it removes units and convert to number |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 149 | It assumes that bandwidth is well formed |
| 150 | Attributes: |
| 151 | 'data': dictionary bottle.FormsDict variable to be checked. None or empty is consideted valid |
| 152 | 'reverse': by default convert form str to int (Mbps), if True it convert from number to units |
| 153 | Return: |
| 154 | None |
| 155 | ''' |
| 156 | if type(data) is dict: |
| 157 | for k in data.keys(): |
| 158 | if type(data[k]) is dict or type(data[k]) is tuple or type(data[k]) is list: |
| tierno | 44528e4 | 2016-10-11 12:06:25 +0000 | [diff] [blame] | 159 | _convert_bandwidth(data[k], reverse, logger) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 160 | if "bandwidth" in data: |
| 161 | try: |
| 162 | value=str(data["bandwidth"]) |
| 163 | if not reverse: |
| 164 | pos = value.find("bps") |
| 165 | if pos>0: |
| 166 | if value[pos-1]=="G": data["bandwidth"] = int(data["bandwidth"][:pos-1]) * 1000 |
| 167 | elif value[pos-1]=="k": data["bandwidth"]= int(data["bandwidth"][:pos-1]) / 1000 |
| 168 | else: data["bandwidth"]= int(data["bandwidth"][:pos-1]) |
| 169 | else: |
| 170 | value = int(data["bandwidth"]) |
| 171 | if value % 1000 == 0: data["bandwidth"]=str(value/1000) + " Gbps" |
| 172 | else: data["bandwidth"]=str(value) + " Mbps" |
| 173 | except: |
| tierno | 44528e4 | 2016-10-11 12:06:25 +0000 | [diff] [blame] | 174 | if logger: |
| 175 | logger.error("convert_bandwidth exception for type '%s' data '%s'", type(data["bandwidth"]), data["bandwidth"]) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 176 | return |
| 177 | if type(data) is tuple or type(data) is list: |
| 178 | for k in data: |
| 179 | if type(k) is dict or type(k) is tuple or type(k) is list: |
| tierno | 44528e4 | 2016-10-11 12:06:25 +0000 | [diff] [blame] | 180 | _convert_bandwidth(k, reverse, logger) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 181 | |
| 182 | def _convert_str2boolean(data, items): |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 183 | '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 184 | Done recursively |
| 185 | Attributes: |
| 186 | 'data': dictionary variable to be checked. None or empty is considered valid |
| 187 | 'items': tuple of keys to convert |
| 188 | Return: |
| 189 | None |
| 190 | ''' |
| 191 | if type(data) is dict: |
| 192 | for k in data.keys(): |
| 193 | if type(data[k]) is dict or type(data[k]) is tuple or type(data[k]) is list: |
| 194 | _convert_str2boolean(data[k], items) |
| 195 | if k in items: |
| 196 | if type(data[k]) is str: |
| montesmoreno | 2a1fc4e | 2017-01-09 16:46:04 +0000 | [diff] [blame] | 197 | if data[k]=="false" or data[k]=="False" or data[k]=="0": data[k]=False |
| 198 | elif data[k]=="true" or data[k]=="True" or data[k]=="1": data[k]=True |
| 199 | elif type(data[k]) is int: |
| 200 | if data[k]==0: data[k]=False |
| 201 | elif data[k]==1: data[k]=True |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 202 | if type(data) is tuple or type(data) is list: |
| 203 | for k in data: |
| 204 | if type(k) is dict or type(k) is tuple or type(k) is list: |
| 205 | _convert_str2boolean(k, items) |
| 206 | |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 207 | class db_base_Exception(httperrors.HttpMappedError): |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 208 | '''Common Exception for all database exceptions''' |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 209 | |
| 210 | def __init__(self, message, http_code=httperrors.Bad_Request): |
| 211 | super(db_base_Exception, self).__init__(message, http_code) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 212 | |
| 213 | class db_base(): |
| 214 | tables_with_created_field=() |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 215 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 216 | def __init__(self, host=None, user=None, passwd=None, database=None, |
| 217 | log_name='db', log_level=None, lock=None): |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 218 | self.host = host |
| 219 | self.user = user |
| 220 | self.passwd = passwd |
| 221 | self.database = database |
| 222 | self.con = None |
| 223 | self.log_level=log_level |
| 224 | self.logger = logging.getLogger(log_name) |
| tierno | b13f3cc | 2016-09-26 10:14:44 +0200 | [diff] [blame] | 225 | if self.log_level: |
| 226 | self.logger.setLevel( getattr(logging, log_level) ) |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 227 | self.lock = lock or Lock() |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 228 | |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 229 | def connect(self, host=None, user=None, passwd=None, database=None): |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 230 | '''Connect to specific data base. |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 231 | The first time a valid host, user, passwd and database must be provided, |
| 232 | Following calls can skip this parameters |
| 233 | ''' |
| 234 | try: |
| 235 | if host: self.host = host |
| 236 | if user: self.user = user |
| 237 | if passwd: self.passwd = passwd |
| 238 | if database: self.database = database |
| 239 | |
| 240 | self.con = mdb.connect(self.host, self.user, self.passwd, self.database) |
| tierno | 44528e4 | 2016-10-11 12:06:25 +0000 | [diff] [blame] | 241 | self.logger.debug("DB: connected to '%s' at '%s@%s'", self.database, self.user, self.host) |
| 242 | except mdb.Error as e: |
| 243 | raise db_base_Exception("Cannot connect to DataBase '{}' at '{}@{}' Error {}: {}".format( |
| 244 | self.database, self.user, self.host, e.args[0], e.args[1]), |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 245 | http_code = httperrors.Unauthorized ) |
| 246 | |
| 247 | def escape(self, value): |
| 248 | return self.con.escape(value) |
| 249 | |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 250 | def escape_string(self, value): |
| Anderson Bravalheri | 0ea9b0f | 2019-02-01 10:56:55 +0000 | [diff] [blame] | 251 | if isinstance(value, unicode): |
| tierno | a3ebc36 | 2018-12-05 16:23:38 +0000 | [diff] [blame] | 252 | value = value.encode("utf8") |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 253 | return self.con.escape_string(value) |
| 254 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 255 | @retry |
| 256 | @with_transaction |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 257 | def get_db_version(self): |
| 258 | ''' Obtain the database schema version. |
| 259 | Return: (negative, text) if error or version 0.0 where schema_version table is missing |
| 260 | (version_int, version_text) if ok |
| 261 | ''' |
| 262 | cmd = "SELECT version_int,version FROM schema_version" |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 263 | self.logger.debug(cmd) |
| 264 | self.cur.execute(cmd) |
| 265 | rows = self.cur.fetchall() |
| 266 | highest_version_int=0 |
| 267 | highest_version="" |
| 268 | for row in rows: #look for the latest version |
| 269 | if row[0]>highest_version_int: |
| 270 | highest_version_int, highest_version = row[0:2] |
| 271 | return highest_version_int, highest_version |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 272 | |
| 273 | def disconnect(self): |
| 274 | '''disconnect from specific data base''' |
| 275 | try: |
| 276 | self.con.close() |
| 277 | self.con = None |
| 278 | except mdb.Error as e: |
| 279 | self.logger.error("while disconnecting from DB: Error %d: %s",e.args[0], e.args[1]) |
| 280 | return |
| 281 | except AttributeError as e: #self.con not defined |
| 282 | if e[0][-5:] == "'con'": |
| 283 | self.logger.warn("while disconnecting from DB: Error %d: %s",e.args[0], e.args[1]) |
| 284 | return |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 285 | else: |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 286 | raise |
| 287 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 288 | def reconnect(self): |
| 289 | """Try to gracefully to the database in case of error""" |
| 290 | try: |
| 291 | self.con.ping(True) # auto-reconnect if the server is available |
| 292 | except: |
| 293 | # The server is probably not available... |
| 294 | # Let's wait a bit |
| 295 | time.sleep(RECOVERY_TIME) |
| 296 | self.con = None |
| 297 | self.connect() |
| 298 | |
| 299 | def fork_connection(self): |
| 300 | """Return a new database object, with a separated connection to the |
| 301 | database (and lock), so it can act independently |
| 302 | """ |
| 303 | obj = self.__class__( |
| 304 | host=self.host, |
| 305 | user=self.user, |
| 306 | passwd=self.passwd, |
| 307 | database=self.database, |
| 308 | log_name=self.logger.name, |
| 309 | log_level=self.log_level, |
| 310 | lock=Lock() |
| 311 | ) |
| 312 | |
| 313 | obj.connect() |
| 314 | |
| 315 | return obj |
| 316 | |
| 317 | @contextmanager |
| 318 | def transaction(self, cursor_type=None): |
| 319 | """DB changes that are executed inside this context will be |
| 320 | automatically rolled back in case of error. |
| 321 | |
| 322 | This implementation also adds a lock, so threads sharing the same |
| 323 | connection object are synchronized. |
| 324 | |
| 325 | Arguments: |
| 326 | cursor_type: default: MySQLdb.cursors.DictCursor |
| 327 | |
| 328 | Yields: |
| 329 | Cursor object |
| 330 | |
| 331 | References: |
| 332 | https://www.oreilly.com/library/view/mysql-cookbook-2nd/059652708X/ch15s08.html |
| 333 | https://github.com/PyMySQL/mysqlclient-python/commit/c64915b1e5c705f4fb10e86db5dcfed0b58552cc |
| 334 | """ |
| 335 | # Previously MySQLdb had built-in support for that using the context |
| 336 | # API for the connection object. |
| 337 | # This support was removed in version 1.40 |
| 338 | # https://github.com/PyMySQL/mysqlclient-python/blob/master/HISTORY.rst#whats-new-in-140 |
| 339 | with self.lock: |
| 340 | try: |
| 341 | if self.con.get_autocommit(): |
| 342 | self.con.query("BEGIN") |
| 343 | |
| 344 | self.cur = self.con.cursor(cursor_type) |
| 345 | yield self.cur |
| 346 | except: # noqa |
| 347 | self.con.rollback() |
| 348 | raise |
| 349 | else: |
| 350 | self.con.commit() |
| 351 | |
| 352 | |
| 353 | def _format_error(self, e, tries=1, command=None, |
| 354 | extra=None, table=None, cmd=None, **_): |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 355 | '''Creates a text error base on the produced exception |
| 356 | Params: |
| 357 | e: mdb exception |
| 358 | retry: in case of timeout, if reconnecting to database and retry, or raise and exception |
| 359 | cmd: database command that produce the exception |
| 360 | command: if the intention is update or delete |
| 361 | extra: extra information to add to some commands |
| 362 | Return |
| 363 | HTTP error in negative, formatted error text |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 364 | ''' # the **_ ignores extra kwargs |
| 365 | table_info = ' (table `{}`)'.format(table) if table else '' |
| 366 | if cmd: |
| 367 | self.logger.debug("Exception '%s' with command '%s'%s", |
| 368 | e, cmd, table_info) |
| 369 | |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 370 | if isinstance(e,AttributeError ): |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 371 | self.logger.debug(str(e), exc_info=True) |
| 372 | raise db_base_Exception("DB Exception " + str(e), httperrors.Internal_Server_Error) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 373 | if e.args[0]==2006 or e.args[0]==2013 : #MySQL server has gone away (((or))) Exception 2013: Lost connection to MySQL server during query |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 374 | # Let's aways reconnect if the connection is lost |
| 375 | # so future calls are not affected. |
| 376 | self.reconnect() |
| 377 | |
| 378 | if tries > 1: |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 379 | self.logger.warn("DB Exception '%s'. Retry", str(e)) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 380 | return |
| 381 | else: |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 382 | raise db_base_Exception("Database connection timeout Try Again", httperrors.Request_Timeout) |
| 383 | |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 384 | fk=e.args[1].find("foreign key constraint fails") |
| 385 | if fk>=0: |
| 386 | if command=="update": |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 387 | raise db_base_Exception("tenant_id '{}' not found.".format(extra), httperrors.Not_Found) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 388 | elif command=="delete": |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 389 | raise db_base_Exception("Resource is not free. There are {} that prevent deleting it.".format(extra), httperrors.Conflict) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 390 | de = e.args[1].find("Duplicate entry") |
| 391 | fk = e.args[1].find("for key") |
| 392 | uk = e.args[1].find("Unknown column") |
| 393 | wc = e.args[1].find("in 'where clause'") |
| 394 | fl = e.args[1].find("in 'field list'") |
| 395 | #print de, fk, uk, wc,fl |
| 396 | if de>=0: |
| 397 | if fk>=0: #error 1062 |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 398 | raise db_base_Exception( |
| 399 | "Value {} already in use for {}{}".format( |
| 400 | e.args[1][de+15:fk], e.args[1][fk+7:], table_info), |
| 401 | httperrors.Conflict) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 402 | if uk>=0: |
| 403 | if wc>=0: |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 404 | raise db_base_Exception( |
| 405 | "Field {} can not be used for filtering{}".format( |
| 406 | e.args[1][uk+14:wc], table_info), |
| 407 | httperrors.Bad_Request) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 408 | if fl>=0: |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 409 | raise db_base_Exception( |
| 410 | "Field {} does not exist{}".format( |
| 411 | e.args[1][uk+14:wc], table_info), |
| 412 | httperrors.Bad_Request) |
| 413 | raise db_base_Exception( |
| 414 | "Database internal Error{} {}: {}".format( |
| 415 | table_info, e.args[0], e.args[1]), |
| 416 | httperrors.Internal_Server_Error) |
| 417 | |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 418 | def __str2db_format(self, data): |
| tierno | a3ebc36 | 2018-12-05 16:23:38 +0000 | [diff] [blame] | 419 | """Convert string data to database format. |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 420 | If data is None it returns the 'Null' text, |
| 421 | otherwise it returns the text surrounded by quotes ensuring internal quotes are escaped. |
| tierno | a3ebc36 | 2018-12-05 16:23:38 +0000 | [diff] [blame] | 422 | """ |
| 423 | if data is None: |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 424 | return 'Null' |
| tierno | a3ebc36 | 2018-12-05 16:23:38 +0000 | [diff] [blame] | 425 | elif isinstance(data[1], (str, unicode)): |
| tierno | 86fad56 | 2017-04-05 19:53:54 +0200 | [diff] [blame] | 426 | return json.dumps(data) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 427 | else: |
| 428 | return json.dumps(str(data)) |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 429 | |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 430 | def __tuple2db_format_set(self, data): |
| tierno | 868220c | 2017-09-26 00:11:05 +0200 | [diff] [blame] | 431 | """Compose the needed text for a SQL SET, parameter 'data' is a pair tuple (A,B), |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 432 | and it returns the text 'A="B"', where A is a field of a table and B is the value |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 433 | If B is None it returns the 'A=Null' text, without surrounding Null by quotes |
| 434 | If B is not None it returns the text "A='B'" or 'A="B"' where B is surrounded by quotes, |
| 435 | and it ensures internal quotes of B are escaped. |
| tierno | 868220c | 2017-09-26 00:11:05 +0200 | [diff] [blame] | 436 | B can be also a dict with special keys: |
| 437 | {"INCREMENT": NUMBER}, then it produce "A=A+NUMBER" |
| 438 | """ |
| tierno | a3ebc36 | 2018-12-05 16:23:38 +0000 | [diff] [blame] | 439 | if data[1] is None: |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 440 | return str(data[0]) + "=Null" |
| tierno | a3ebc36 | 2018-12-05 16:23:38 +0000 | [diff] [blame] | 441 | elif isinstance(data[1], (str, unicode)): |
| tierno | 86fad56 | 2017-04-05 19:53:54 +0200 | [diff] [blame] | 442 | return str(data[0]) + '=' + json.dumps(data[1]) |
| tierno | 868220c | 2017-09-26 00:11:05 +0200 | [diff] [blame] | 443 | elif isinstance(data[1], dict): |
| 444 | if "INCREMENT" in data[1]: |
| 445 | return "{A}={A}{N:+d}".format(A=data[0], N=data[1]["INCREMENT"]) |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 446 | raise db_base_Exception("Format error for UPDATE field: {!r}".format(data[0])) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 447 | else: |
| 448 | return str(data[0]) + '=' + json.dumps(str(data[1])) |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 449 | |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 450 | def __create_where(self, data, use_or=None): |
| 451 | """ |
| 452 | Compose the needed text for a SQL WHERE, parameter 'data' can be a dict or a list of dict. By default lists are |
| 453 | concatenated with OR and dict with AND, unless parameter 'use_or' indicates other thing. |
| 454 | If a dict it will generate 'key1="value1" AND key2="value2" AND ...'. |
| 455 | If value is None, it will produce 'key is null' |
| 456 | If value is a list or tuple, it will produce 'key="value[0]" OR key="value[1]" OR ...' |
| tierno | 16e3dd4 | 2018-04-24 12:52:40 +0200 | [diff] [blame] | 457 | keys can be suffixed by >,<,<>,>=,<=,' LIKE ' so that this is used to compare key and value instead of "=" |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 458 | The special keys "OR", "AND" with a dict value is used to create a nested WHERE |
| 459 | If a list, each item will be a dictionary that will be concatenated with OR by default |
| 460 | :param data: dict or list of dicts |
| 461 | :param use_or: Can be None (use default behaviour), True (use OR) or False (use AND) |
| 462 | :return: a string with the content to send to mysql |
| 463 | """ |
| 464 | cmd = [] |
| 465 | if isinstance(data, dict): |
| 466 | for k, v in data.items(): |
| 467 | if k == "OR": |
| 468 | cmd.append("(" + self.__create_where(v, use_or=True) + ")") |
| 469 | continue |
| 470 | elif k == "AND": |
| 471 | cmd.append("(" + self.__create_where(v, use_or=False) + ")") |
| 472 | continue |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 473 | |
| tierno | 16e3dd4 | 2018-04-24 12:52:40 +0200 | [diff] [blame] | 474 | if k.endswith(">") or k.endswith("<") or k.endswith("=") or k.endswith(" LIKE "): |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 475 | pass |
| 476 | else: |
| 477 | k += "=" |
| 478 | |
| 479 | if v is None: |
| 480 | cmd.append(k.replace("=", " is").replace("<>", " is not") + " Null") |
| 481 | elif isinstance(v, (tuple, list)): |
| 482 | cmd2 = [] |
| 483 | for v2 in v: |
| 484 | if v2 is None: |
| 485 | cmd2.append(k.replace("=", " is").replace("<>", " is not") + " Null") |
| tierno | a3ebc36 | 2018-12-05 16:23:38 +0000 | [diff] [blame] | 486 | elif isinstance(v2, (str, unicode)): |
| 487 | cmd2.append(k + json.dumps(v2)) |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 488 | else: |
| 489 | cmd2.append(k + json.dumps(str(v2))) |
| 490 | cmd.append("(" + " OR ".join(cmd2) + ")") |
| tierno | a3ebc36 | 2018-12-05 16:23:38 +0000 | [diff] [blame] | 491 | elif isinstance(v, (str, unicode)): |
| 492 | cmd.append(k + json.dumps(v)) |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 493 | else: |
| 494 | cmd.append(k + json.dumps(str(v))) |
| 495 | elif isinstance(data, (tuple, list)): |
| 496 | if use_or is None: |
| 497 | use_or = True |
| 498 | for k in data: |
| 499 | cmd.append("(" + self.__create_where(k) + ")") |
| tierno | 86fad56 | 2017-04-05 19:53:54 +0200 | [diff] [blame] | 500 | else: |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 501 | raise db_base_Exception("invalid WHERE clause at '{}'".format(data)) |
| 502 | if use_or: |
| 503 | return " OR ".join(cmd) |
| 504 | return " AND ".join(cmd) |
| 505 | |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 506 | def __remove_quotes(self, data): |
| 507 | '''remove single quotes ' of any string content of data dictionary''' |
| 508 | for k,v in data.items(): |
| 509 | if type(v) == str: |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 510 | if "'" in v: |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 511 | data[k] = data[k].replace("'","_") |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 512 | |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 513 | def _update_rows(self, table, UPDATE, WHERE, modified_time=0): |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 514 | """ Update one or several rows of a table. |
| 515 | :param UPDATE: dictionary with the changes. dict keys are database columns that will be set with the dict values |
| 516 | :param table: database table to update |
| 517 | :param WHERE: dict or list of dicts to compose the SQL WHERE clause. |
| 518 | If a dict it will generate 'key1="value1" AND key2="value2" AND ...'. |
| 519 | If value is None, it will produce 'key is null' |
| 520 | If value is a list or tuple, it will produce 'key="value[0]" OR key="value[1]" OR ...' |
| 521 | keys can be suffixed by >,<,<>,>=,<= so that this is used to compare key and value instead of "=" |
| 522 | The special keys "OR", "AND" with a dict value is used to create a nested WHERE |
| 523 | If a list, each item will be a dictionary that will be concatenated with OR |
| 524 | :return: the number of updated rows, raises exception upon error |
| 525 | """ |
| 526 | # gettting uuid |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 527 | values = ",".join(map(self.__tuple2db_format_set, UPDATE.iteritems() )) |
| 528 | if modified_time: |
| 529 | values += ",modified_at={:f}".format(modified_time) |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 530 | cmd= "UPDATE " + table + " SET " + values + " WHERE " + self.__create_where(WHERE) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 531 | self.logger.debug(cmd) |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 532 | self.cur.execute(cmd) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 533 | return self.cur.rowcount |
| tierno | 8e69032 | 2017-08-10 15:58:50 +0200 | [diff] [blame] | 534 | |
| 535 | def _new_uuid(self, root_uuid=None, used_table=None, created_time=0): |
| 536 | """ |
| 537 | Generate a new uuid. It DOES NOT begin or end the transaction, so self.con.cursor must be created |
| 538 | :param root_uuid: master uuid of the transaction |
| 539 | :param used_table: the table this uuid is intended for |
| 540 | :param created_time: time of creation |
| 541 | :return: the created uuid |
| 542 | """ |
| 543 | |
| 544 | uuid = str(myUuid.uuid1()) |
| 545 | # defining root_uuid if not provided |
| 546 | if root_uuid is None: |
| 547 | root_uuid = uuid |
| 548 | if created_time: |
| 549 | created_at = created_time |
| 550 | else: |
| 551 | created_at = time.time() |
| 552 | # inserting new uuid |
| 553 | cmd = "INSERT INTO uuids (uuid, root_uuid, used_at, created_at) VALUES ('{:s}','{:s}','{:s}', {:f})".format( |
| 554 | uuid, root_uuid, used_table, created_at) |
| 555 | self.logger.debug(cmd) |
| 556 | self.cur.execute(cmd) |
| 557 | return uuid |
| 558 | |
| gcalvino | c62cfa5 | 2017-10-05 18:21:25 +0200 | [diff] [blame] | 559 | def _new_row_internal(self, table, INSERT, add_uuid=False, root_uuid=None, created_time=0, confidential_data=False): |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 560 | ''' Add one row into a table. It DOES NOT begin or end the transaction, so self.con.cursor must be created |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 561 | Attribute |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 562 | INSERT: dictionary with the key:value to insert |
| 563 | table: table where to insert |
| 564 | add_uuid: if True, it will create an uuid key entry at INSERT if not provided |
| tierno | 3c44e7b | 2019-03-04 17:32:01 +0000 | [diff] [blame] | 565 | created_time: time to add to the created_at column |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 566 | It checks presence of uuid and add one automatically otherwise |
| 567 | Return: uuid |
| 568 | ''' |
| 569 | |
| 570 | if add_uuid: |
| 571 | #create uuid if not provided |
| 572 | if 'uuid' not in INSERT: |
| 573 | uuid = INSERT['uuid'] = str(myUuid.uuid1()) # create_uuid |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 574 | else: |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 575 | uuid = str(INSERT['uuid']) |
| 576 | else: |
| 577 | uuid=None |
| 578 | if add_uuid: |
| 579 | #defining root_uuid if not provided |
| 580 | if root_uuid is None: |
| 581 | root_uuid = uuid |
| 582 | if created_time: |
| 583 | created_at = created_time |
| 584 | else: |
| 585 | created_at=time.time() |
| 586 | #inserting new uuid |
| 587 | cmd = "INSERT INTO uuids (uuid, root_uuid, used_at, created_at) VALUES ('{:s}','{:s}','{:s}', {:f})".format(uuid, root_uuid, table, created_at) |
| 588 | self.logger.debug(cmd) |
| 589 | self.cur.execute(cmd) |
| 590 | #insertion |
| 591 | cmd= "INSERT INTO " + table +" SET " + \ |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 592 | ",".join(map(self.__tuple2db_format_set, INSERT.iteritems() )) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 593 | if created_time: |
| tierno | 1ee6043 | 2019-04-26 12:26:01 +0000 | [diff] [blame] | 594 | cmd += ",created_at={time:.9f},modified_at={time:.9f}".format(time=created_time) |
| gcalvino | c62cfa5 | 2017-10-05 18:21:25 +0200 | [diff] [blame] | 595 | if confidential_data: |
| 596 | index = cmd.find("SET") |
| 597 | subcmd = cmd[:index] + 'SET...' |
| 598 | self.logger.debug(subcmd) |
| 599 | else: |
| 600 | self.logger.debug(cmd) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 601 | self.cur.execute(cmd) |
| 602 | self.cur.rowcount |
| 603 | return uuid |
| 604 | |
| 605 | def _get_rows(self,table,uuid): |
| 606 | cmd = "SELECT * FROM {} WHERE uuid='{}'".format(str(table), str(uuid)) |
| 607 | self.logger.debug(cmd) |
| 608 | self.cur.execute(cmd) |
| 609 | rows = self.cur.fetchall() |
| 610 | return rows |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 611 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 612 | @retry |
| 613 | @with_transaction |
| gcalvino | c62cfa5 | 2017-10-05 18:21:25 +0200 | [diff] [blame] | 614 | def new_row(self, table, INSERT, add_uuid=False, created_time=0, confidential_data=False): |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 615 | ''' Add one row into a table. |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 616 | Attribute |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 617 | INSERT: dictionary with the key: value to insert |
| 618 | table: table where to insert |
| 619 | tenant_id: only useful for logs. If provided, logs will use this tenant_id |
| 620 | add_uuid: if True, it will create an uuid key entry at INSERT if not provided |
| 621 | It checks presence of uuid and add one automatically otherwise |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 622 | Return: uuid |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 623 | ''' |
| 624 | if table in self.tables_with_created_field and created_time==0: |
| 625 | created_time=time.time() |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 626 | return self._new_row_internal(table, INSERT, add_uuid, None, created_time, confidential_data) |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 627 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 628 | @retry |
| 629 | @with_transaction |
| tierno | 3c44e7b | 2019-03-04 17:32:01 +0000 | [diff] [blame] | 630 | def update_rows(self, table, UPDATE, WHERE, modified_time=None, attempt=_ATTEMPT): |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 631 | """ Update one or several rows of a table. |
| 632 | :param UPDATE: dictionary with the changes. dict keys are database columns that will be set with the dict values |
| 633 | :param table: database table to update |
| 634 | :param WHERE: dict or list of dicts to compose the SQL WHERE clause. |
| 635 | If a dict it will generate 'key1="value1" AND key2="value2" AND ...'. |
| 636 | If value is None, it will produce 'key is null' |
| 637 | If value is a list or tuple, it will produce 'key="value[0]" OR key="value[1]" OR ...' |
| 638 | keys can be suffixed by >,<,<>,>=,<= so that this is used to compare key and value instead of "=" |
| 639 | The special keys "OR", "AND" with a dict value is used to create a nested WHERE |
| 640 | If a list, each item will be a dictionary that will be concatenated with OR |
| tierno | 3c44e7b | 2019-03-04 17:32:01 +0000 | [diff] [blame] | 641 | :param modified_time: Can contain the time to be set to the table row. |
| 642 | None to set automatically, 0 to do not modify it |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 643 | :return: the number of updated rows, raises exception upon error |
| 644 | """ |
| tierno | 3c44e7b | 2019-03-04 17:32:01 +0000 | [diff] [blame] | 645 | if table in self.tables_with_created_field and modified_time is None: |
| 646 | modified_time = time.time() |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 647 | |
| 648 | return self._update_rows(table, UPDATE, WHERE, modified_time) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 649 | |
| tierno | fc5f80b | 2018-05-29 16:00:43 +0200 | [diff] [blame] | 650 | def _delete_row_by_id_internal(self, table, uuid): |
| 651 | cmd = "DELETE FROM {} WHERE uuid = '{}'".format(table, uuid) |
| 652 | self.logger.debug(cmd) |
| 653 | self.cur.execute(cmd) |
| 654 | deleted = self.cur.rowcount |
| 655 | # delete uuid |
| 656 | self.cur = self.con.cursor() |
| 657 | cmd = "DELETE FROM uuids WHERE root_uuid = '{}'".format(uuid) |
| 658 | self.logger.debug(cmd) |
| 659 | self.cur.execute(cmd) |
| 660 | return deleted |
| 661 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 662 | @retry(command='delete', extra='dependencies') |
| 663 | @with_transaction |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 664 | def delete_row_by_id(self, table, uuid): |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 665 | return self._delete_row_by_id_internal(table, uuid) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 666 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 667 | @retry |
| 668 | def delete_row(self, attempt=_ATTEMPT, **sql_dict): |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 669 | """ Deletes rows from a table. |
| 670 | :param UPDATE: dictionary with the changes. dict keys are database columns that will be set with the dict values |
| 671 | :param FROM: string with table name (Mandatory) |
| 672 | :param WHERE: dict or list of dicts to compose the SQL WHERE clause. (Optional) |
| 673 | If a dict it will generate 'key1="value1" AND key2="value2" AND ...'. |
| 674 | If value is None, it will produce 'key is null' |
| 675 | If value is a list or tuple, it will produce 'key="value[0]" OR key="value[1]" OR ...' |
| 676 | keys can be suffixed by >,<,<>,>=,<= so that this is used to compare key and value instead of "=" |
| 677 | The special keys "OR", "AND" with a dict value is used to create a nested WHERE |
| 678 | If a list, each item will be a dictionary that will be concatenated with OR |
| 679 | :return: the number of deleted rows, raises exception upon error |
| 680 | """ |
| 681 | # print sql_dict |
| 682 | cmd = "DELETE FROM " + str(sql_dict['FROM']) |
| 683 | if sql_dict.get('WHERE'): |
| 684 | cmd += " WHERE " + self.__create_where(sql_dict['WHERE']) |
| 685 | if sql_dict.get('LIMIT'): |
| 686 | cmd += " LIMIT " + str(sql_dict['LIMIT']) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 687 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 688 | attempt.info['cmd'] = cmd |
| 689 | |
| 690 | with self.transaction(): |
| 691 | self.logger.debug(cmd) |
| 692 | self.cur.execute(cmd) |
| 693 | deleted = self.cur.rowcount |
| 694 | return deleted |
| 695 | |
| 696 | @retry |
| 697 | @with_transaction(cursor='dict') |
| 698 | def get_rows_by_id(self, table, uuid, attempt=_ATTEMPT): |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 699 | '''get row from a table based on uuid''' |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 700 | cmd="SELECT * FROM {} where uuid='{}'".format(str(table), str(uuid)) |
| 701 | attempt.info['cmd'] = cmd |
| 702 | self.logger.debug(cmd) |
| 703 | self.cur.execute(cmd) |
| 704 | rows = self.cur.fetchall() |
| 705 | return rows |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 706 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 707 | @retry |
| 708 | def get_rows(self, attempt=_ATTEMPT, **sql_dict): |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 709 | """ Obtain rows from a table. |
| 710 | :param SELECT: list or tuple of fields to retrieve) (by default all) |
| 711 | :param FROM: string with table name (Mandatory) |
| 712 | :param WHERE: dict or list of dicts to compose the SQL WHERE clause. (Optional) |
| 713 | If a dict it will generate 'key1="value1" AND key2="value2" AND ...'. |
| 714 | If value is None, it will produce 'key is null' |
| 715 | If value is a list or tuple, it will produce 'key="value[0]" OR key="value[1]" OR ...' |
| 716 | keys can be suffixed by >,<,<>,>=,<= so that this is used to compare key and value instead of "=" |
| 717 | The special keys "OR", "AND" with a dict value is used to create a nested WHERE |
| 718 | If a list, each item will be a dictionary that will be concatenated with OR |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 719 | :param LIMIT: limit the number of obtained entries (Optional) |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 720 | :param ORDER_BY: list or tuple of fields to order, add ' DESC' to each item if inverse order is required |
| 721 | :return: a list with dictionaries at each row, raises exception upon error |
| 722 | """ |
| 723 | # print sql_dict |
| 724 | cmd = "SELECT " |
| 725 | if 'SELECT' in sql_dict: |
| 726 | if isinstance(sql_dict['SELECT'], (tuple, list)): |
| 727 | cmd += ",".join(map(str, sql_dict['SELECT'])) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 728 | else: |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 729 | cmd += sql_dict['SELECT'] |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 730 | else: |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 731 | cmd += "*" |
| 732 | |
| 733 | cmd += " FROM " + str(sql_dict['FROM']) |
| 734 | if sql_dict.get('WHERE'): |
| 735 | cmd += " WHERE " + self.__create_where(sql_dict['WHERE']) |
| 736 | |
| 737 | if 'ORDER_BY' in sql_dict: |
| 738 | cmd += " ORDER BY " |
| 739 | if isinstance(sql_dict['ORDER_BY'], (tuple, list)): |
| 740 | cmd += ",".join(map(str, sql_dict['ORDER_BY'])) |
| 741 | else: |
| 742 | cmd += str(sql_dict['ORDER_BY']) |
| 743 | |
| 744 | if 'LIMIT' in sql_dict: |
| 745 | cmd += " LIMIT " + str(sql_dict['LIMIT']) |
| 746 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 747 | attempt.info['cmd'] = cmd |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 748 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 749 | with self.transaction(mdb.cursors.DictCursor): |
| 750 | self.logger.debug(cmd) |
| 751 | self.cur.execute(cmd) |
| 752 | rows = self.cur.fetchall() |
| 753 | return rows |
| 754 | |
| 755 | @retry |
| garciadeblas | 4a1659a | 2019-03-21 09:55:44 +0100 | [diff] [blame] | 756 | def get_table_by_uuid_name(self, table, uuid_name, error_item_text=None, allow_several=False, WHERE_OR={}, WHERE_AND_OR="OR", attempt=_ATTEMPT): |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 757 | ''' Obtain One row from a table based on name or uuid. |
| 758 | Attribute: |
| 759 | table: string of table name |
| 760 | uuid_name: name or uuid. If not uuid format is found, it is considered a name |
| garciadeblas | 4a1659a | 2019-03-21 09:55:44 +0100 | [diff] [blame] | 761 | allow_several: if False return ERROR if more than one row are found |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 762 | error_item_text: in case of error it identifies the 'item' name for a proper output text |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 763 | 'WHERE_OR': dict of key:values, translated to key=value OR ... (Optional) |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 764 | 'WHERE_AND_OR: str 'AND' or 'OR'(by default) mark the priority to 'WHERE AND (WHERE_OR)' or (WHERE) OR WHERE_OR' (Optional |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 765 | Return: if allow_several==False, a dictionary with this row, or error if no item is found or more than one is found |
| 766 | if allow_several==True, a list of dictionaries with the row or rows, error if no item is found |
| 767 | ''' |
| 768 | |
| 769 | if error_item_text==None: |
| 770 | error_item_text = table |
| 771 | what = 'uuid' if af.check_valid_uuid(uuid_name) else 'name' |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 772 | cmd = " SELECT * FROM {} WHERE {}='{}'".format(table, what, uuid_name) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 773 | if WHERE_OR: |
| tierno | 3fcfdb7 | 2017-10-24 07:48:24 +0200 | [diff] [blame] | 774 | where_or = self.__create_where(WHERE_OR, use_or=True) |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 775 | if WHERE_AND_OR == "AND": |
| 776 | cmd += " AND (" + where_or + ")" |
| 777 | else: |
| 778 | cmd += " OR " + where_or |
| 779 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 780 | attempt.info['cmd'] = cmd |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 781 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 782 | with self.transaction(mdb.cursors.DictCursor): |
| 783 | self.logger.debug(cmd) |
| 784 | self.cur.execute(cmd) |
| 785 | number = self.cur.rowcount |
| 786 | if number == 0: |
| 787 | raise db_base_Exception("No {} found with {} '{}'".format(error_item_text, what, uuid_name), http_code=httperrors.Not_Found) |
| garciadeblas | 4a1659a | 2019-03-21 09:55:44 +0100 | [diff] [blame] | 788 | elif number > 1 and not allow_several: |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 789 | raise db_base_Exception("More than one {} found with {} '{}'".format(error_item_text, what, uuid_name), http_code=httperrors.Conflict) |
| garciadeblas | 4a1659a | 2019-03-21 09:55:44 +0100 | [diff] [blame] | 790 | if allow_several: |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 791 | rows = self.cur.fetchall() |
| 792 | else: |
| 793 | rows = self.cur.fetchone() |
| 794 | return rows |
| 795 | |
| 796 | @retry(table='uuids') |
| 797 | @with_transaction(cursor='dict') |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 798 | def get_uuid(self, uuid): |
| 799 | '''check in the database if this uuid is already present''' |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 800 | self.cur.execute("SELECT * FROM uuids where uuid='" + str(uuid) + "'") |
| 801 | rows = self.cur.fetchall() |
| 802 | return self.cur.rowcount, rows |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 803 | |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 804 | @retry |
| 805 | @with_transaction(cursor='dict') |
| tierno | f97fd27 | 2016-07-11 14:32:37 +0200 | [diff] [blame] | 806 | def get_uuid_from_name(self, table, name): |
| 807 | '''Searchs in table the name and returns the uuid |
| Anderson Bravalheri | 0446cd5 | 2018-08-17 15:26:19 +0100 | [diff] [blame] | 808 | ''' |
| Anderson Bravalheri | dfed511 | 2019-02-08 01:44:14 +0000 | [diff] [blame] | 809 | where_text = "name='" + name +"'" |
| 810 | self.cur.execute("SELECT * FROM " + table + " WHERE "+ where_text) |
| 811 | rows = self.cur.fetchall() |
| 812 | if self.cur.rowcount==0: |
| 813 | return 0, "Name %s not found in table %s" %(name, table) |
| 814 | elif self.cur.rowcount>1: |
| 815 | return self.cur.rowcount, "More than one VNF with name %s found in table %s" %(name, table) |
| 816 | return self.cur.rowcount, rows[0]["uuid"] |