blob: e6e1134d854f21383114c35aa19a93f9eda85931 [file] [log] [blame]
tiernof97fd272016-07-11 14:32:37 +02001# -*- coding: utf-8 -*-
2
3##
tierno92021022018-09-12 16:29:23 +02004# Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U.
tiernof97fd272016-07-11 14:32:37 +02005# 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'''
25Base class for openmano database manipulation
26'''
27__author__="Alfonso Tierno"
28__date__ ="$4-Apr-2016 10:05:01$"
29
30import MySQLdb as mdb
31import uuid as myUuid
32import utils as af
33import json
34#import yaml
35import time
36import logging
37import datetime
Anderson Bravalheridfed5112019-02-08 01:44:14 +000038from contextlib import contextmanager
39from functools import wraps, partial
40from threading import Lock
tiernof97fd272016-07-11 14:32:37 +020041from jsonschema import validate as js_v, exceptions as js_e
42
Anderson Bravalheri0446cd52018-08-17 15:26:19 +010043from .http_tools import errors as httperrors
Anderson Bravalheridfed5112019-02-08 01:44:14 +000044from .utils import Attempt, get_arg, inject_args
45
46
47RECOVERY_TIME = 3
48
49_ATTEMPT = Attempt()
50
51
52def 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
79def 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
tiernof97fd272016-07-11 14:32:37 +0200117
118def _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
132def _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 Bravalheri0446cd52018-08-17 15:26:19 +0100140 elif type(v) is dict or type(v) is list or type(v) is tuple:
tiernof97fd272016-07-11 14:32:37 +0200141 _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
tierno44528e42016-10-11 12:06:25 +0000147def _convert_bandwidth(data, reverse=False, logger=None):
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100148 '''Check the field bandwidth recursivelly and when found, it removes units and convert to number
tiernof97fd272016-07-11 14:32:37 +0200149 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:
tierno44528e42016-10-11 12:06:25 +0000159 _convert_bandwidth(data[k], reverse, logger)
tiernof97fd272016-07-11 14:32:37 +0200160 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:
tierno44528e42016-10-11 12:06:25 +0000174 if logger:
175 logger.error("convert_bandwidth exception for type '%s' data '%s'", type(data["bandwidth"]), data["bandwidth"])
tiernof97fd272016-07-11 14:32:37 +0200176 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:
tierno44528e42016-10-11 12:06:25 +0000180 _convert_bandwidth(k, reverse, logger)
tiernof97fd272016-07-11 14:32:37 +0200181
182def _convert_str2boolean(data, items):
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100183 '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
tiernof97fd272016-07-11 14:32:37 +0200184 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:
montesmoreno2a1fc4e2017-01-09 16:46:04 +0000197 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
tiernof97fd272016-07-11 14:32:37 +0200202 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 Bravalheri0446cd52018-08-17 15:26:19 +0100207class db_base_Exception(httperrors.HttpMappedError):
tiernof97fd272016-07-11 14:32:37 +0200208 '''Common Exception for all database exceptions'''
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100209
210 def __init__(self, message, http_code=httperrors.Bad_Request):
211 super(db_base_Exception, self).__init__(message, http_code)
tiernof97fd272016-07-11 14:32:37 +0200212
213class db_base():
214 tables_with_created_field=()
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100215
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000216 def __init__(self, host=None, user=None, passwd=None, database=None,
217 log_name='db', log_level=None, lock=None):
tiernof97fd272016-07-11 14:32:37 +0200218 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)
tiernob13f3cc2016-09-26 10:14:44 +0200225 if self.log_level:
226 self.logger.setLevel( getattr(logging, log_level) )
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000227 self.lock = lock or Lock()
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100228
tiernof97fd272016-07-11 14:32:37 +0200229 def connect(self, host=None, user=None, passwd=None, database=None):
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100230 '''Connect to specific data base.
tiernof97fd272016-07-11 14:32:37 +0200231 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)
tierno44528e42016-10-11 12:06:25 +0000241 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 Bravalheri0446cd52018-08-17 15:26:19 +0100245 http_code = httperrors.Unauthorized )
246
247 def escape(self, value):
248 return self.con.escape(value)
249
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100250 def escape_string(self, value):
Anderson Bravalheri0ea9b0f2019-02-01 10:56:55 +0000251 if isinstance(value, unicode):
tiernoa3ebc362018-12-05 16:23:38 +0000252 value = value.encode("utf8")
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100253 return self.con.escape_string(value)
254
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000255 @retry
256 @with_transaction
tiernof97fd272016-07-11 14:32:37 +0200257 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 Bravalheridfed5112019-02-08 01:44:14 +0000263 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
tiernof97fd272016-07-11 14:32:37 +0200272
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 Bravalheri0446cd52018-08-17 15:26:19 +0100285 else:
tiernof97fd272016-07-11 14:32:37 +0200286 raise
287
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000288 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, **_):
tiernof97fd272016-07-11 14:32:37 +0200355 '''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 Bravalheridfed5112019-02-08 01:44:14 +0000364 ''' # 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
tiernof97fd272016-07-11 14:32:37 +0200370 if isinstance(e,AttributeError ):
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100371 self.logger.debug(str(e), exc_info=True)
372 raise db_base_Exception("DB Exception " + str(e), httperrors.Internal_Server_Error)
tiernof97fd272016-07-11 14:32:37 +0200373 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 Bravalheridfed5112019-02-08 01:44:14 +0000374 # Let's aways reconnect if the connection is lost
375 # so future calls are not affected.
376 self.reconnect()
377
378 if tries > 1:
tiernof97fd272016-07-11 14:32:37 +0200379 self.logger.warn("DB Exception '%s'. Retry", str(e))
tiernof97fd272016-07-11 14:32:37 +0200380 return
381 else:
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100382 raise db_base_Exception("Database connection timeout Try Again", httperrors.Request_Timeout)
383
tiernof97fd272016-07-11 14:32:37 +0200384 fk=e.args[1].find("foreign key constraint fails")
385 if fk>=0:
386 if command=="update":
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100387 raise db_base_Exception("tenant_id '{}' not found.".format(extra), httperrors.Not_Found)
tiernof97fd272016-07-11 14:32:37 +0200388 elif command=="delete":
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100389 raise db_base_Exception("Resource is not free. There are {} that prevent deleting it.".format(extra), httperrors.Conflict)
tiernof97fd272016-07-11 14:32:37 +0200390 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 Bravalheri0446cd52018-08-17 15:26:19 +0100398 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)
tiernof97fd272016-07-11 14:32:37 +0200402 if uk>=0:
403 if wc>=0:
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100404 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)
tiernof97fd272016-07-11 14:32:37 +0200408 if fl>=0:
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100409 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
tiernof97fd272016-07-11 14:32:37 +0200418 def __str2db_format(self, data):
tiernoa3ebc362018-12-05 16:23:38 +0000419 """Convert string data to database format.
tiernof97fd272016-07-11 14:32:37 +0200420 If data is None it returns the 'Null' text,
421 otherwise it returns the text surrounded by quotes ensuring internal quotes are escaped.
tiernoa3ebc362018-12-05 16:23:38 +0000422 """
423 if data is None:
tiernof97fd272016-07-11 14:32:37 +0200424 return 'Null'
tiernoa3ebc362018-12-05 16:23:38 +0000425 elif isinstance(data[1], (str, unicode)):
tierno86fad562017-04-05 19:53:54 +0200426 return json.dumps(data)
tiernof97fd272016-07-11 14:32:37 +0200427 else:
428 return json.dumps(str(data))
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100429
tiernof97fd272016-07-11 14:32:37 +0200430 def __tuple2db_format_set(self, data):
tierno868220c2017-09-26 00:11:05 +0200431 """Compose the needed text for a SQL SET, parameter 'data' is a pair tuple (A,B),
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100432 and it returns the text 'A="B"', where A is a field of a table and B is the value
tiernof97fd272016-07-11 14:32:37 +0200433 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.
tierno868220c2017-09-26 00:11:05 +0200436 B can be also a dict with special keys:
437 {"INCREMENT": NUMBER}, then it produce "A=A+NUMBER"
438 """
tiernoa3ebc362018-12-05 16:23:38 +0000439 if data[1] is None:
tiernof97fd272016-07-11 14:32:37 +0200440 return str(data[0]) + "=Null"
tiernoa3ebc362018-12-05 16:23:38 +0000441 elif isinstance(data[1], (str, unicode)):
tierno86fad562017-04-05 19:53:54 +0200442 return str(data[0]) + '=' + json.dumps(data[1])
tierno868220c2017-09-26 00:11:05 +0200443 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 Bravalheri0446cd52018-08-17 15:26:19 +0100446 raise db_base_Exception("Format error for UPDATE field: {!r}".format(data[0]))
tiernof97fd272016-07-11 14:32:37 +0200447 else:
448 return str(data[0]) + '=' + json.dumps(str(data[1]))
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100449
tierno3fcfdb72017-10-24 07:48:24 +0200450 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 ...'
tierno16e3dd42018-04-24 12:52:40 +0200457 keys can be suffixed by >,<,<>,>=,<=,' LIKE ' so that this is used to compare key and value instead of "="
tierno3fcfdb72017-10-24 07:48:24 +0200458 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
tiernof97fd272016-07-11 14:32:37 +0200473
tierno16e3dd42018-04-24 12:52:40 +0200474 if k.endswith(">") or k.endswith("<") or k.endswith("=") or k.endswith(" LIKE "):
tierno3fcfdb72017-10-24 07:48:24 +0200475 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")
tiernoa3ebc362018-12-05 16:23:38 +0000486 elif isinstance(v2, (str, unicode)):
487 cmd2.append(k + json.dumps(v2))
tierno3fcfdb72017-10-24 07:48:24 +0200488 else:
489 cmd2.append(k + json.dumps(str(v2)))
490 cmd.append("(" + " OR ".join(cmd2) + ")")
tiernoa3ebc362018-12-05 16:23:38 +0000491 elif isinstance(v, (str, unicode)):
492 cmd.append(k + json.dumps(v))
tierno3fcfdb72017-10-24 07:48:24 +0200493 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) + ")")
tierno86fad562017-04-05 19:53:54 +0200500 else:
tierno3fcfdb72017-10-24 07:48:24 +0200501 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
tiernof97fd272016-07-11 14:32:37 +0200506 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 Bravalheri0446cd52018-08-17 15:26:19 +0100510 if "'" in v:
tiernof97fd272016-07-11 14:32:37 +0200511 data[k] = data[k].replace("'","_")
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100512
tiernof97fd272016-07-11 14:32:37 +0200513 def _update_rows(self, table, UPDATE, WHERE, modified_time=0):
tierno3fcfdb72017-10-24 07:48:24 +0200514 """ 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
tiernof97fd272016-07-11 14:32:37 +0200527 values = ",".join(map(self.__tuple2db_format_set, UPDATE.iteritems() ))
528 if modified_time:
529 values += ",modified_at={:f}".format(modified_time)
tierno3fcfdb72017-10-24 07:48:24 +0200530 cmd= "UPDATE " + table + " SET " + values + " WHERE " + self.__create_where(WHERE)
tiernof97fd272016-07-11 14:32:37 +0200531 self.logger.debug(cmd)
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100532 self.cur.execute(cmd)
tiernof97fd272016-07-11 14:32:37 +0200533 return self.cur.rowcount
tierno8e690322017-08-10 15:58:50 +0200534
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
gcalvinoc62cfa52017-10-05 18:21:25 +0200559 def _new_row_internal(self, table, INSERT, add_uuid=False, root_uuid=None, created_time=0, confidential_data=False):
tiernof97fd272016-07-11 14:32:37 +0200560 ''' Add one row into a table. It DOES NOT begin or end the transaction, so self.con.cursor must be created
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100561 Attribute
tiernof97fd272016-07-11 14:32:37 +0200562 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
tierno3c44e7b2019-03-04 17:32:01 +0000565 created_time: time to add to the created_at column
tiernof97fd272016-07-11 14:32:37 +0200566 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 Bravalheri0446cd52018-08-17 15:26:19 +0100574 else:
tiernof97fd272016-07-11 14:32:37 +0200575 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 Bravalheri0446cd52018-08-17 15:26:19 +0100592 ",".join(map(self.__tuple2db_format_set, INSERT.iteritems() ))
tiernof97fd272016-07-11 14:32:37 +0200593 if created_time:
tierno1ee60432019-04-26 12:26:01 +0000594 cmd += ",created_at={time:.9f},modified_at={time:.9f}".format(time=created_time)
gcalvinoc62cfa52017-10-05 18:21:25 +0200595 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)
tiernof97fd272016-07-11 14:32:37 +0200601 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 Bravalheri0446cd52018-08-17 15:26:19 +0100611
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000612 @retry
613 @with_transaction
gcalvinoc62cfa52017-10-05 18:21:25 +0200614 def new_row(self, table, INSERT, add_uuid=False, created_time=0, confidential_data=False):
tiernof97fd272016-07-11 14:32:37 +0200615 ''' Add one row into a table.
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100616 Attribute
tiernof97fd272016-07-11 14:32:37 +0200617 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 Bravalheri0446cd52018-08-17 15:26:19 +0100622 Return: uuid
tiernof97fd272016-07-11 14:32:37 +0200623 '''
624 if table in self.tables_with_created_field and created_time==0:
625 created_time=time.time()
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000626 return self._new_row_internal(table, INSERT, add_uuid, None, created_time, confidential_data)
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100627
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000628 @retry
629 @with_transaction
tierno3c44e7b2019-03-04 17:32:01 +0000630 def update_rows(self, table, UPDATE, WHERE, modified_time=None, attempt=_ATTEMPT):
tierno3fcfdb72017-10-24 07:48:24 +0200631 """ 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
tierno3c44e7b2019-03-04 17:32:01 +0000641 :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
tierno3fcfdb72017-10-24 07:48:24 +0200643 :return: the number of updated rows, raises exception upon error
644 """
tierno3c44e7b2019-03-04 17:32:01 +0000645 if table in self.tables_with_created_field and modified_time is None:
646 modified_time = time.time()
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000647
648 return self._update_rows(table, UPDATE, WHERE, modified_time)
tiernof97fd272016-07-11 14:32:37 +0200649
tiernofc5f80b2018-05-29 16:00:43 +0200650 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 Bravalheridfed5112019-02-08 01:44:14 +0000662 @retry(command='delete', extra='dependencies')
663 @with_transaction
tiernof97fd272016-07-11 14:32:37 +0200664 def delete_row_by_id(self, table, uuid):
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000665 return self._delete_row_by_id_internal(table, uuid)
tiernof97fd272016-07-11 14:32:37 +0200666
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000667 @retry
668 def delete_row(self, attempt=_ATTEMPT, **sql_dict):
tierno3fcfdb72017-10-24 07:48:24 +0200669 """ 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'])
tiernof97fd272016-07-11 14:32:37 +0200687
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000688 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):
tiernof97fd272016-07-11 14:32:37 +0200699 '''get row from a table based on uuid'''
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000700 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 Bravalheri0446cd52018-08-17 15:26:19 +0100706
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000707 @retry
708 def get_rows(self, attempt=_ATTEMPT, **sql_dict):
tierno3fcfdb72017-10-24 07:48:24 +0200709 """ 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 Bravalheri0446cd52018-08-17 15:26:19 +0100719 :param LIMIT: limit the number of obtained entries (Optional)
tierno3fcfdb72017-10-24 07:48:24 +0200720 :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']))
tiernof97fd272016-07-11 14:32:37 +0200728 else:
tierno3fcfdb72017-10-24 07:48:24 +0200729 cmd += sql_dict['SELECT']
tiernof97fd272016-07-11 14:32:37 +0200730 else:
tierno3fcfdb72017-10-24 07:48:24 +0200731 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 Bravalheridfed5112019-02-08 01:44:14 +0000747 attempt.info['cmd'] = cmd
tiernof97fd272016-07-11 14:32:37 +0200748
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000749 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
garciadeblas4a1659a2019-03-21 09:55:44 +0100756 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):
tiernof97fd272016-07-11 14:32:37 +0200757 ''' 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
garciadeblas4a1659a2019-03-21 09:55:44 +0100761 allow_several: if False return ERROR if more than one row are found
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100762 error_item_text: in case of error it identifies the 'item' name for a proper output text
tiernof97fd272016-07-11 14:32:37 +0200763 'WHERE_OR': dict of key:values, translated to key=value OR ... (Optional)
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100764 'WHERE_AND_OR: str 'AND' or 'OR'(by default) mark the priority to 'WHERE AND (WHERE_OR)' or (WHERE) OR WHERE_OR' (Optional
tiernof97fd272016-07-11 14:32:37 +0200765 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'
tierno3fcfdb72017-10-24 07:48:24 +0200772 cmd = " SELECT * FROM {} WHERE {}='{}'".format(table, what, uuid_name)
tiernof97fd272016-07-11 14:32:37 +0200773 if WHERE_OR:
tierno3fcfdb72017-10-24 07:48:24 +0200774 where_or = self.__create_where(WHERE_OR, use_or=True)
tiernof97fd272016-07-11 14:32:37 +0200775 if WHERE_AND_OR == "AND":
776 cmd += " AND (" + where_or + ")"
777 else:
778 cmd += " OR " + where_or
779
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000780 attempt.info['cmd'] = cmd
tiernof97fd272016-07-11 14:32:37 +0200781
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000782 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)
garciadeblas4a1659a2019-03-21 09:55:44 +0100788 elif number > 1 and not allow_several:
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000789 raise db_base_Exception("More than one {} found with {} '{}'".format(error_item_text, what, uuid_name), http_code=httperrors.Conflict)
garciadeblas4a1659a2019-03-21 09:55:44 +0100790 if allow_several:
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000791 rows = self.cur.fetchall()
792 else:
793 rows = self.cur.fetchone()
794 return rows
795
796 @retry(table='uuids')
797 @with_transaction(cursor='dict')
tiernof97fd272016-07-11 14:32:37 +0200798 def get_uuid(self, uuid):
799 '''check in the database if this uuid is already present'''
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000800 self.cur.execute("SELECT * FROM uuids where uuid='" + str(uuid) + "'")
801 rows = self.cur.fetchall()
802 return self.cur.rowcount, rows
tiernof97fd272016-07-11 14:32:37 +0200803
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000804 @retry
805 @with_transaction(cursor='dict')
tiernof97fd272016-07-11 14:32:37 +0200806 def get_uuid_from_name(self, table, name):
807 '''Searchs in table the name and returns the uuid
Anderson Bravalheri0446cd52018-08-17 15:26:19 +0100808 '''
Anderson Bravalheridfed5112019-02-08 01:44:14 +0000809 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"]