Revert "Removing deprecated/unused/outdated code"
[osm/RO.git] / RO / osm_ro / db_base.py
1 # -*- coding: utf-8 -*-
2
3 ##
4 # Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U.
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 from osm_ro import utils as af
33 import json
34 #import yaml
35 import time
36 import logging
37 import datetime
38 from contextlib import contextmanager
39 from functools import wraps, partial
40 from threading import Lock
41 from jsonschema import validate as js_v, exceptions as js_e
42
43 from .http_tools import errors as httperrors
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
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')
140 elif type(v) is dict or type(v) is list or type(v) is tuple:
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
147 def _convert_bandwidth(data, reverse=False, logger=None):
148 '''Check the field bandwidth recursivelly and when found, it removes units and convert to number
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:
159 _convert_bandwidth(data[k], reverse, logger)
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:
172 data["bandwidth"] = str(value // 1000) + " Gbps"
173 else:
174 data["bandwidth"] = str(value) + " Mbps"
175 except:
176 if logger:
177 logger.error("convert_bandwidth exception for type '%s' data '%s'", type(data["bandwidth"]), data["bandwidth"])
178 return
179 if type(data) is tuple or type(data) is list:
180 for k in data:
181 if type(k) is dict or type(k) is tuple or type(k) is list:
182 _convert_bandwidth(k, reverse, logger)
183
184 def _convert_str2boolean(data, items):
185 '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
186 Done recursively
187 Attributes:
188 'data': dictionary variable to be checked. None or empty is considered valid
189 'items': tuple of keys to convert
190 Return:
191 None
192 '''
193 if type(data) is dict:
194 for k in data.keys():
195 if type(data[k]) is dict or type(data[k]) is tuple or type(data[k]) is list:
196 _convert_str2boolean(data[k], items)
197 if k in items:
198 if type(data[k]) is str:
199 if data[k]=="false" or data[k]=="False" or data[k]=="0": data[k]=False
200 elif data[k]=="true" or data[k]=="True" or data[k]=="1": data[k]=True
201 elif type(data[k]) is int:
202 if data[k]==0: data[k]=False
203 elif data[k]==1: data[k]=True
204 if type(data) is tuple or type(data) is list:
205 for k in data:
206 if type(k) is dict or type(k) is tuple or type(k) is list:
207 _convert_str2boolean(k, items)
208
209 class db_base_Exception(httperrors.HttpMappedError):
210 '''Common Exception for all database exceptions'''
211
212 def __init__(self, message, http_code=httperrors.Bad_Request):
213 super(db_base_Exception, self).__init__(message, http_code)
214
215 class db_base():
216 tables_with_created_field=()
217
218 def __init__(self, host=None, user=None, passwd=None, database=None,
219 log_name='db', log_level=None, lock=None):
220 self.host = host
221 self.user = user
222 self.passwd = passwd
223 self.database = database
224 self.con = None
225 self.log_level=log_level
226 self.logger = logging.getLogger(log_name)
227 if self.log_level:
228 self.logger.setLevel( getattr(logging, log_level) )
229 self.lock = lock or Lock()
230
231 def connect(self, host=None, user=None, passwd=None, database=None):
232 '''Connect to specific data base.
233 The first time a valid host, user, passwd and database must be provided,
234 Following calls can skip this parameters
235 '''
236 try:
237 if host: self.host = host
238 if user: self.user = user
239 if passwd: self.passwd = passwd
240 if database: self.database = database
241
242 self.con = mdb.connect(self.host, self.user, self.passwd, self.database)
243 self.logger.debug("DB: connected to '%s' at '%s@%s'", self.database, self.user, self.host)
244 except mdb.Error as e:
245 raise db_base_Exception("Cannot connect to DataBase '{}' at '{}@{}' Error {}: {}".format(
246 self.database, self.user, self.host, e.args[0], e.args[1]),
247 http_code = httperrors.Unauthorized )
248
249 def escape(self, value):
250 return self.con.escape(value)
251
252 def escape_string(self, value):
253 return self.con.escape_string(value)
254
255 @retry
256 @with_transaction
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"
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
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.warning("while disconnecting from DB: Error %d: %s",e.args[0], e.args[1])
284 return
285 else:
286 raise
287
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, **_):
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
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", e, cmd, table_info)
368
369 if isinstance(e,AttributeError ):
370 self.logger.debug(str(e), exc_info=True)
371 raise db_base_Exception("DB Exception " + str(e), httperrors.Internal_Server_Error)
372 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
373 # Let's aways reconnect if the connection is lost
374 # so future calls are not affected.
375 self.reconnect()
376
377 if tries > 1:
378 self.logger.warning("DB Exception '%s'. Retry", str(e))
379 return
380 else:
381 raise db_base_Exception("Database connection timeout Try Again", httperrors.Request_Timeout)
382
383 fk=e.args[1].find("foreign key constraint fails")
384 if fk>=0:
385 if command=="update":
386 raise db_base_Exception("tenant_id '{}' not found.".format(extra), httperrors.Not_Found)
387 elif command=="delete":
388 raise db_base_Exception("Resource is not free. There are {} that prevent deleting it.".format(extra), httperrors.Conflict)
389 de = e.args[1].find("Duplicate entry")
390 fk = e.args[1].find("for key")
391 uk = e.args[1].find("Unknown column")
392 wc = e.args[1].find("in 'where clause'")
393 fl = e.args[1].find("in 'field list'")
394 #print de, fk, uk, wc,fl
395 if de>=0:
396 if fk>=0: #error 1062
397 raise db_base_Exception(
398 "Value {} already in use for {}{}".format(
399 e.args[1][de+15:fk], e.args[1][fk+7:], table_info),
400 httperrors.Conflict)
401 if uk>=0:
402 if wc>=0:
403 raise db_base_Exception(
404 "Field {} can not be used for filtering{}".format(
405 e.args[1][uk+14:wc], table_info),
406 httperrors.Bad_Request)
407 if fl>=0:
408 raise db_base_Exception(
409 "Field {} does not exist{}".format(
410 e.args[1][uk+14:wc], table_info),
411 httperrors.Bad_Request)
412 raise db_base_Exception(
413 "Database internal Error{} {}: {}".format(
414 table_info, e.args[0], e.args[1]),
415 httperrors.Internal_Server_Error)
416
417 def __str2db_format(self, data):
418 """Convert string data to database format.
419 If data is None it returns the 'Null' text,
420 otherwise it returns the text surrounded by quotes ensuring internal quotes are escaped.
421 """
422 if data is None:
423 return 'Null'
424 elif isinstance(data[1], str):
425 return json.dumps(data)
426 else:
427 return json.dumps(str(data))
428
429 def __tuple2db_format_set(self, data):
430 """Compose the needed text for a SQL SET, parameter 'data' is a pair tuple (A,B),
431 and it returns the text 'A="B"', where A is a field of a table and B is the value
432 If B is None it returns the 'A=Null' text, without surrounding Null by quotes
433 If B is not None it returns the text "A='B'" or 'A="B"' where B is surrounded by quotes,
434 and it ensures internal quotes of B are escaped.
435 B can be also a dict with special keys:
436 {"INCREMENT": NUMBER}, then it produce "A=A+NUMBER"
437 """
438 if data[1] is None:
439 return str(data[0]) + "=Null"
440 elif isinstance(data[1], str):
441 return str(data[0]) + '=' + json.dumps(data[1])
442 elif isinstance(data[1], dict):
443 if "INCREMENT" in data[1]:
444 return "{A}={A}{N:+d}".format(A=data[0], N=data[1]["INCREMENT"])
445 raise db_base_Exception("Format error for UPDATE field: {!r}".format(data[0]))
446 else:
447 return str(data[0]) + '=' + json.dumps(str(data[1]))
448
449 def __create_where(self, data, use_or=None):
450 """
451 Compose the needed text for a SQL WHERE, parameter 'data' can be a dict or a list of dict. By default lists are
452 concatenated with OR and dict with AND, unless parameter 'use_or' indicates other thing.
453 If a dict it will generate 'key1="value1" AND key2="value2" AND ...'.
454 If value is None, it will produce 'key is null'
455 If value is a list or tuple, it will produce 'key="value[0]" OR key="value[1]" OR ...'
456 keys can be suffixed by >,<,<>,>=,<=,' LIKE ' so that this is used to compare key and value instead of "="
457 The special keys "OR", "AND" with a dict value is used to create a nested WHERE
458 If a list, each item will be a dictionary that will be concatenated with OR by default
459 :param data: dict or list of dicts
460 :param use_or: Can be None (use default behaviour), True (use OR) or False (use AND)
461 :return: a string with the content to send to mysql
462 """
463 cmd = []
464 if isinstance(data, dict):
465 for k, v in data.items():
466 if k == "OR":
467 cmd.append("(" + self.__create_where(v, use_or=True) + ")")
468 continue
469 elif k == "AND":
470 cmd.append("(" + self.__create_where(v, use_or=False) + ")")
471 continue
472
473 if k.endswith(">") or k.endswith("<") or k.endswith("=") or k.endswith(" LIKE "):
474 pass
475 else:
476 k += "="
477
478 if v is None:
479 cmd.append(k.replace("=", " is").replace("<>", " is not") + " Null")
480 elif isinstance(v, (tuple, list)):
481 cmd2 = []
482 for v2 in v:
483 if v2 is None:
484 cmd2.append(k.replace("=", " is").replace("<>", " is not") + " Null")
485 elif isinstance(v2, str):
486 cmd2.append(k + json.dumps(v2))
487 else:
488 cmd2.append(k + json.dumps(str(v2)))
489 cmd.append("(" + " OR ".join(cmd2) + ")")
490 elif isinstance(v, str):
491 cmd.append(k + json.dumps(v))
492 else:
493 cmd.append(k + json.dumps(str(v)))
494 elif isinstance(data, (tuple, list)):
495 if use_or is None:
496 use_or = True
497 for k in data:
498 cmd.append("(" + self.__create_where(k) + ")")
499 else:
500 raise db_base_Exception("invalid WHERE clause at '{}'".format(data))
501 if use_or:
502 return " OR ".join(cmd)
503 return " AND ".join(cmd)
504
505 def __remove_quotes(self, data):
506 '''remove single quotes ' of any string content of data dictionary'''
507 for k,v in data.items():
508 if type(v) == str:
509 if "'" in v:
510 data[k] = data[k].replace("'","_")
511
512 def _update_rows(self, table, UPDATE, WHERE, modified_time=0):
513 """ Update one or several rows of a table.
514 :param UPDATE: dictionary with the changes. dict keys are database columns that will be set with the dict values
515 :param table: database table to update
516 :param WHERE: dict or list of dicts to compose the SQL WHERE clause.
517 If a dict it will generate 'key1="value1" AND key2="value2" AND ...'.
518 If value is None, it will produce 'key is null'
519 If value is a list or tuple, it will produce 'key="value[0]" OR key="value[1]" OR ...'
520 keys can be suffixed by >,<,<>,>=,<= so that this is used to compare key and value instead of "="
521 The special keys "OR", "AND" with a dict value is used to create a nested WHERE
522 If a list, each item will be a dictionary that will be concatenated with OR
523 :return: the number of updated rows, raises exception upon error
524 """
525 # gettting uuid
526 values = ",".join(map(self.__tuple2db_format_set, UPDATE.items() ))
527 if modified_time:
528 values += "{}modified_at={:f}".format("," if values else "", modified_time)
529 cmd = "UPDATE " + table + " SET " + values + " WHERE " + self.__create_where(WHERE)
530 self.logger.debug(cmd)
531 self.cur.execute(cmd)
532 return self.cur.rowcount
533
534 def _new_uuid(self, root_uuid=None, used_table=None, created_time=0):
535 """
536 Generate a new uuid. It DOES NOT begin or end the transaction, so self.con.cursor must be created
537 :param root_uuid: master uuid of the transaction
538 :param used_table: the table this uuid is intended for
539 :param created_time: time of creation
540 :return: the created uuid
541 """
542
543 uuid = str(myUuid.uuid1())
544 # defining root_uuid if not provided
545 if root_uuid is None:
546 root_uuid = uuid
547 if created_time:
548 created_at = created_time
549 else:
550 created_at = time.time()
551 # inserting new uuid
552 cmd = "INSERT INTO uuids (uuid, root_uuid, used_at, created_at) VALUES ('{:s}','{:s}','{:s}', {:f})".format(
553 uuid, root_uuid, used_table, created_at)
554 self.logger.debug(cmd)
555 self.cur.execute(cmd)
556 return uuid
557
558 def _new_row_internal(self, table, INSERT, add_uuid=False, root_uuid=None, created_time=0, confidential_data=False):
559 ''' Add one row into a table. It DOES NOT begin or end the transaction, so self.con.cursor must be created
560 Attribute
561 INSERT: dictionary with the key:value to insert
562 table: table where to insert
563 add_uuid: if True, it will create an uuid key entry at INSERT if not provided
564 created_time: time to add to the created_at column
565 It checks presence of uuid and add one automatically otherwise
566 Return: uuid
567 '''
568
569 if add_uuid:
570 #create uuid if not provided
571 if 'uuid' not in INSERT:
572 uuid = INSERT['uuid'] = str(myUuid.uuid1()) # create_uuid
573 else:
574 uuid = str(INSERT['uuid'])
575 else:
576 uuid=None
577 if add_uuid:
578 #defining root_uuid if not provided
579 if root_uuid is None:
580 root_uuid = uuid
581 if created_time:
582 created_at = created_time
583 else:
584 created_at=time.time()
585 #inserting new uuid
586 cmd = "INSERT INTO uuids (uuid, root_uuid, used_at, created_at) VALUES ('{:s}','{:s}','{:s}', {:f})".format(uuid, root_uuid, table, created_at)
587 self.logger.debug(cmd)
588 self.cur.execute(cmd)
589 #insertion
590 cmd= "INSERT INTO " + table +" SET " + \
591 ",".join(map(self.__tuple2db_format_set, INSERT.items() ))
592 if created_time:
593 cmd += ",created_at={time:.9f},modified_at={time:.9f}".format(time=created_time)
594 if confidential_data:
595 index = cmd.find("SET")
596 subcmd = cmd[:index] + 'SET...'
597 self.logger.debug(subcmd)
598 else:
599 self.logger.debug(cmd)
600 self.cur.execute(cmd)
601 self.cur.rowcount
602 return uuid
603
604 def _get_rows(self,table,uuid):
605 cmd = "SELECT * FROM {} WHERE uuid='{}'".format(str(table), str(uuid))
606 self.logger.debug(cmd)
607 self.cur.execute(cmd)
608 rows = self.cur.fetchall()
609 return rows
610
611 @retry
612 @with_transaction
613 def new_row(self, table, INSERT, add_uuid=False, created_time=0, confidential_data=False):
614 ''' Add one row into a table.
615 Attribute
616 INSERT: dictionary with the key: value to insert
617 table: table where to insert
618 tenant_id: only useful for logs. If provided, logs will use this tenant_id
619 add_uuid: if True, it will create an uuid key entry at INSERT if not provided
620 It checks presence of uuid and add one automatically otherwise
621 Return: uuid
622 '''
623 if table in self.tables_with_created_field and created_time==0:
624 created_time=time.time()
625 return self._new_row_internal(table, INSERT, add_uuid, None, created_time, confidential_data)
626
627 @retry
628 @with_transaction
629 def update_rows(self, table, UPDATE, WHERE, modified_time=None, attempt=_ATTEMPT):
630 """ Update one or several rows of a table.
631 :param UPDATE: dictionary with the changes. dict keys are database columns that will be set with the dict values
632 :param table: database table to update
633 :param WHERE: dict or list of dicts to compose the SQL WHERE clause.
634 If a dict it will generate 'key1="value1" AND key2="value2" AND ...'.
635 If value is None, it will produce 'key is null'
636 If value is a list or tuple, it will produce 'key="value[0]" OR key="value[1]" OR ...'
637 keys can be suffixed by >,<,<>,>=,<= so that this is used to compare key and value instead of "="
638 The special keys "OR", "AND" with a dict value is used to create a nested WHERE
639 If a list, each item will be a dictionary that will be concatenated with OR
640 :param modified_time: Can contain the time to be set to the table row.
641 None to set automatically, 0 to do not modify it
642 :return: the number of updated rows, raises exception upon error
643 """
644 if table in self.tables_with_created_field and modified_time is None:
645 modified_time = time.time()
646
647 return self._update_rows(table, UPDATE, WHERE, modified_time)
648
649 def _delete_row_by_id_internal(self, table, uuid):
650 cmd = "DELETE FROM {} WHERE uuid = '{}'".format(table, uuid)
651 self.logger.debug(cmd)
652 self.cur.execute(cmd)
653 deleted = self.cur.rowcount
654 # delete uuid
655 self.cur = self.con.cursor()
656 cmd = "DELETE FROM uuids WHERE root_uuid = '{}'".format(uuid)
657 self.logger.debug(cmd)
658 self.cur.execute(cmd)
659 return deleted
660
661 @retry(command='delete', extra='dependencies')
662 @with_transaction
663 def delete_row_by_id(self, table, uuid):
664 return self._delete_row_by_id_internal(table, uuid)
665
666 @retry
667 def delete_row(self, attempt=_ATTEMPT, **sql_dict):
668 """ Deletes rows from a table.
669 :param UPDATE: dictionary with the changes. dict keys are database columns that will be set with the dict values
670 :param FROM: string with table name (Mandatory)
671 :param WHERE: dict or list of dicts to compose the SQL WHERE clause. (Optional)
672 If a dict it will generate 'key1="value1" AND key2="value2" AND ...'.
673 If value is None, it will produce 'key is null'
674 If value is a list or tuple, it will produce 'key="value[0]" OR key="value[1]" OR ...'
675 keys can be suffixed by >,<,<>,>=,<= so that this is used to compare key and value instead of "="
676 The special keys "OR", "AND" with a dict value is used to create a nested WHERE
677 If a list, each item will be a dictionary that will be concatenated with OR
678 :return: the number of deleted rows, raises exception upon error
679 """
680 # print sql_dict
681 cmd = "DELETE FROM " + str(sql_dict['FROM'])
682 if sql_dict.get('WHERE'):
683 cmd += " WHERE " + self.__create_where(sql_dict['WHERE'])
684 if sql_dict.get('LIMIT'):
685 cmd += " LIMIT " + str(sql_dict['LIMIT'])
686
687 attempt.info['cmd'] = cmd
688
689 with self.transaction():
690 self.logger.debug(cmd)
691 self.cur.execute(cmd)
692 deleted = self.cur.rowcount
693 return deleted
694
695 @retry
696 @with_transaction(cursor='dict')
697 def get_rows_by_id(self, table, uuid, attempt=_ATTEMPT):
698 '''get row from a table based on uuid'''
699 cmd="SELECT * FROM {} where uuid='{}'".format(str(table), str(uuid))
700 attempt.info['cmd'] = cmd
701 self.logger.debug(cmd)
702 self.cur.execute(cmd)
703 rows = self.cur.fetchall()
704 return rows
705
706 @retry
707 def get_rows(self, attempt=_ATTEMPT, **sql_dict):
708 """ Obtain rows from a table.
709 :param SELECT: list or tuple of fields to retrieve) (by default all)
710 :param FROM: string with table name (Mandatory)
711 :param WHERE: dict or list of dicts to compose the SQL WHERE clause. (Optional)
712 If a dict it will generate 'key1="value1" AND key2="value2" AND ...'.
713 If value is None, it will produce 'key is null'
714 If value is a list or tuple, it will produce 'key="value[0]" OR key="value[1]" OR ...'
715 keys can be suffixed by >,<,<>,>=,<= so that this is used to compare key and value instead of "="
716 The special keys "OR", "AND" with a dict value is used to create a nested WHERE
717 If a list, each item will be a dictionary that will be concatenated with OR
718 :param LIMIT: limit the number of obtained entries (Optional)
719 :param ORDER_BY: list or tuple of fields to order, add ' DESC' to each item if inverse order is required
720 :return: a list with dictionaries at each row, raises exception upon error
721 """
722 # print sql_dict
723 cmd = "SELECT "
724 if 'SELECT' in sql_dict:
725 if isinstance(sql_dict['SELECT'], (tuple, list)):
726 cmd += ",".join(map(str, sql_dict['SELECT']))
727 else:
728 cmd += sql_dict['SELECT']
729 else:
730 cmd += "*"
731
732 cmd += " FROM " + str(sql_dict['FROM'])
733 if sql_dict.get('WHERE'):
734 cmd += " WHERE " + self.__create_where(sql_dict['WHERE'])
735
736 if 'ORDER_BY' in sql_dict:
737 cmd += " ORDER BY "
738 if isinstance(sql_dict['ORDER_BY'], (tuple, list)):
739 cmd += ",".join(map(str, sql_dict['ORDER_BY']))
740 else:
741 cmd += str(sql_dict['ORDER_BY'])
742
743 if 'LIMIT' in sql_dict:
744 cmd += " LIMIT " + str(sql_dict['LIMIT'])
745
746 attempt.info['cmd'] = cmd
747
748 with self.transaction(mdb.cursors.DictCursor):
749 self.logger.debug(cmd)
750 self.cur.execute(cmd)
751 rows = self.cur.fetchall()
752 return rows
753
754 @retry
755 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):
756 ''' Obtain One row from a table based on name or uuid.
757 Attribute:
758 table: string of table name
759 uuid_name: name or uuid. If not uuid format is found, it is considered a name
760 allow_several: if False return ERROR if more than one row are found
761 error_item_text: in case of error it identifies the 'item' name for a proper output text
762 'WHERE_OR': dict of key:values, translated to key=value OR ... (Optional)
763 'WHERE_AND_OR: str 'AND' or 'OR'(by default) mark the priority to 'WHERE AND (WHERE_OR)' or (WHERE) OR WHERE_OR' (Optional
764 Return: if allow_several==False, a dictionary with this row, or error if no item is found or more than one is found
765 if allow_several==True, a list of dictionaries with the row or rows, error if no item is found
766 '''
767
768 if error_item_text==None:
769 error_item_text = table
770 what = 'uuid' if af.check_valid_uuid(uuid_name) else 'name'
771 cmd = " SELECT * FROM {} WHERE {}='{}'".format(table, what, uuid_name)
772 if WHERE_OR:
773 where_or = self.__create_where(WHERE_OR, use_or=True)
774 if WHERE_AND_OR == "AND":
775 cmd += " AND (" + where_or + ")"
776 else:
777 cmd += " OR " + where_or
778
779 attempt.info['cmd'] = cmd
780
781 with self.transaction(mdb.cursors.DictCursor):
782 self.logger.debug(cmd)
783 self.cur.execute(cmd)
784 number = self.cur.rowcount
785 if number == 0:
786 raise db_base_Exception("No {} found with {} '{}'".format(error_item_text, what, uuid_name), http_code=httperrors.Not_Found)
787 elif number > 1 and not allow_several:
788 raise db_base_Exception("More than one {} found with {} '{}'".format(error_item_text, what, uuid_name), http_code=httperrors.Conflict)
789 if allow_several:
790 rows = self.cur.fetchall()
791 else:
792 rows = self.cur.fetchone()
793 return rows
794
795 @retry(table='uuids')
796 @with_transaction(cursor='dict')
797 def get_uuid(self, uuid):
798 '''check in the database if this uuid is already present'''
799 self.cur.execute("SELECT * FROM uuids where uuid='" + str(uuid) + "'")
800 rows = self.cur.fetchall()
801 return self.cur.rowcount, rows
802
803 @retry
804 @with_transaction(cursor='dict')
805 def get_uuid_from_name(self, table, name):
806 '''Searchs in table the name and returns the uuid
807 '''
808 where_text = "name='" + name +"'"
809 self.cur.execute("SELECT * FROM " + table + " WHERE "+ where_text)
810 rows = self.cur.fetchall()
811 if self.cur.rowcount==0:
812 return 0, "Name {} not found in table {}".format(name, table)
813 elif self.cur.rowcount>1:
814 return self.cur.rowcount, "More than one VNF with name {} found in table {}".format(name, table)
815 return self.cur.rowcount, rows[0]["uuid"]