inject_user_key routine fixes
[osm/RO.git] / 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 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: data["bandwidth"]=str(value/1000) + " Gbps"
172 else: data["bandwidth"]=str(value) + " Mbps"
173 except:
174 if logger:
175 logger.error("convert_bandwidth exception for type '%s' data '%s'", type(data["bandwidth"]), data["bandwidth"])
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:
180 _convert_bandwidth(k, reverse, logger)
181
182 def _convert_str2boolean(data, items):
183 '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
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:
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
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
207 class db_base_Exception(httperrors.HttpMappedError):
208 '''Common Exception for all database exceptions'''
209
210 def __init__(self, message, http_code=httperrors.Bad_Request):
211 super(db_base_Exception, self).__init__(message, http_code)
212
213 class db_base():
214 tables_with_created_field=()
215
216 def __init__(self, host=None, user=None, passwd=None, database=None,
217 log_name='db', log_level=None, lock=None):
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)
225 if self.log_level:
226 self.logger.setLevel( getattr(logging, log_level) )
227 self.lock = lock or Lock()
228
229 def connect(self, host=None, user=None, passwd=None, database=None):
230 '''Connect to specific data base.
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)
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]),
245 http_code = httperrors.Unauthorized )
246
247 def escape(self, value):
248 return self.con.escape(value)
249
250 def escape_string(self, value):
251 if isinstance(value, unicode):
252 value = value.encode("utf8")
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.warn("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",
368 e, cmd, table_info)
369
370 if isinstance(e,AttributeError ):
371 self.logger.debug(str(e), exc_info=True)
372 raise db_base_Exception("DB Exception " + str(e), httperrors.Internal_Server_Error)
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
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:
379 self.logger.warn("DB Exception '%s'. Retry", str(e))
380 return
381 else:
382 raise db_base_Exception("Database connection timeout Try Again", httperrors.Request_Timeout)
383
384 fk=e.args[1].find("foreign key constraint fails")
385 if fk>=0:
386 if command=="update":
387 raise db_base_Exception("tenant_id '{}' not found.".format(extra), httperrors.Not_Found)
388 elif command=="delete":
389 raise db_base_Exception("Resource is not free. There are {} that prevent deleting it.".format(extra), httperrors.Conflict)
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
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)
402 if uk>=0:
403 if wc>=0:
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)
408 if fl>=0:
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
418 def __str2db_format(self, data):
419 """Convert string data to database format.
420 If data is None it returns the 'Null' text,
421 otherwise it returns the text surrounded by quotes ensuring internal quotes are escaped.
422 """
423 if data is None:
424 return 'Null'
425 elif isinstance(data[1], (str, unicode)):
426 return json.dumps(data)
427 else:
428 return json.dumps(str(data))
429
430 def __tuple2db_format_set(self, data):
431 """Compose the needed text for a SQL SET, parameter 'data' is a pair tuple (A,B),
432 and it returns the text 'A="B"', where A is a field of a table and B is the value
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.
436 B can be also a dict with special keys:
437 {"INCREMENT": NUMBER}, then it produce "A=A+NUMBER"
438 """
439 if data[1] is None:
440 return str(data[0]) + "=Null"
441 elif isinstance(data[1], (str, unicode)):
442 return str(data[0]) + '=' + json.dumps(data[1])
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"])
446 raise db_base_Exception("Format error for UPDATE field: {!r}".format(data[0]))
447 else:
448 return str(data[0]) + '=' + json.dumps(str(data[1]))
449
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 ...'
457 keys can be suffixed by >,<,<>,>=,<=,' LIKE ' so that this is used to compare key and value instead of "="
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
473
474 if k.endswith(">") or k.endswith("<") or k.endswith("=") or k.endswith(" LIKE "):
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")
486 elif isinstance(v2, (str, unicode)):
487 cmd2.append(k + json.dumps(v2))
488 else:
489 cmd2.append(k + json.dumps(str(v2)))
490 cmd.append("(" + " OR ".join(cmd2) + ")")
491 elif isinstance(v, (str, unicode)):
492 cmd.append(k + json.dumps(v))
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) + ")")
500 else:
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
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:
510 if "'" in v:
511 data[k] = data[k].replace("'","_")
512
513 def _update_rows(self, table, UPDATE, WHERE, modified_time=0):
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
527 values = ",".join(map(self.__tuple2db_format_set, UPDATE.iteritems() ))
528 if modified_time:
529 values += ",modified_at={:f}".format(modified_time)
530 cmd= "UPDATE " + table + " SET " + values + " WHERE " + self.__create_where(WHERE)
531 self.logger.debug(cmd)
532 self.cur.execute(cmd)
533 return self.cur.rowcount
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
559 def _new_row_internal(self, table, INSERT, add_uuid=False, root_uuid=None, created_time=0, confidential_data=False):
560 ''' Add one row into a table. It DOES NOT begin or end the transaction, so self.con.cursor must be created
561 Attribute
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
565 created_time: time to add to the created_at column
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
574 else:
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 " + \
592 ",".join(map(self.__tuple2db_format_set, INSERT.iteritems() ))
593 if created_time:
594 cmd += ",created_at={time:.9f},modified_at={time:.9f}".format(time=created_time)
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)
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
611
612 @retry
613 @with_transaction
614 def new_row(self, table, INSERT, add_uuid=False, created_time=0, confidential_data=False):
615 ''' Add one row into a table.
616 Attribute
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
622 Return: uuid
623 '''
624 if table in self.tables_with_created_field and created_time==0:
625 created_time=time.time()
626 return self._new_row_internal(table, INSERT, add_uuid, None, created_time, confidential_data)
627
628 @retry
629 @with_transaction
630 def update_rows(self, table, UPDATE, WHERE, modified_time=None, attempt=_ATTEMPT):
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
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
643 :return: the number of updated rows, raises exception upon error
644 """
645 if table in self.tables_with_created_field and modified_time is None:
646 modified_time = time.time()
647
648 return self._update_rows(table, UPDATE, WHERE, modified_time)
649
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
662 @retry(command='delete', extra='dependencies')
663 @with_transaction
664 def delete_row_by_id(self, table, uuid):
665 return self._delete_row_by_id_internal(table, uuid)
666
667 @retry
668 def delete_row(self, attempt=_ATTEMPT, **sql_dict):
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'])
687
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):
699 '''get row from a table based on uuid'''
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
706
707 @retry
708 def get_rows(self, attempt=_ATTEMPT, **sql_dict):
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
719 :param LIMIT: limit the number of obtained entries (Optional)
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']))
728 else:
729 cmd += sql_dict['SELECT']
730 else:
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
747 attempt.info['cmd'] = cmd
748
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
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):
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
761 allow_several: if False return ERROR if more than one row are found
762 error_item_text: in case of error it identifies the 'item' name for a proper output text
763 'WHERE_OR': dict of key:values, translated to key=value OR ... (Optional)
764 'WHERE_AND_OR: str 'AND' or 'OR'(by default) mark the priority to 'WHERE AND (WHERE_OR)' or (WHERE) OR WHERE_OR' (Optional
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'
772 cmd = " SELECT * FROM {} WHERE {}='{}'".format(table, what, uuid_name)
773 if WHERE_OR:
774 where_or = self.__create_where(WHERE_OR, use_or=True)
775 if WHERE_AND_OR == "AND":
776 cmd += " AND (" + where_or + ")"
777 else:
778 cmd += " OR " + where_or
779
780 attempt.info['cmd'] = cmd
781
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)
788 elif number > 1 and not allow_several:
789 raise db_base_Exception("More than one {} found with {} '{}'".format(error_item_text, what, uuid_name), http_code=httperrors.Conflict)
790 if allow_several:
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')
798 def get_uuid(self, uuid):
799 '''check in the database if this uuid is already present'''
800 self.cur.execute("SELECT * FROM uuids where uuid='" + str(uuid) + "'")
801 rows = self.cur.fetchall()
802 return self.cur.rowcount, rows
803
804 @retry
805 @with_transaction(cursor='dict')
806 def get_uuid_from_name(self, table, name):
807 '''Searchs in table the name and returns the uuid
808 '''
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"]