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