1 # -*- coding: utf-8 -*-
4 # Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U.
5 # This file is part of openmano
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
12 # http://www.apache.org/licenses/LICENSE-2.0
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
20 # For those usages not covered by the Apache License, Version 2.0 please
21 # contact with: nfvlabs@tid.es
25 Base class for openmano database manipulation
27 __author__
="Alfonso Tierno"
28 __date__
="$4-Apr-2016 10:05:01$"
38 from jsonschema
import validate
as js_v
, exceptions
as js_e
40 HTTP_Bad_Request
= 400
41 HTTP_Unauthorized
= 401
43 HTTP_Method_Not_Allowed
= 405
44 HTTP_Request_Timeout
= 408
46 HTTP_Service_Unavailable
= 503
47 HTTP_Internal_Server_Error
= 500
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}$"}
55 except js_e
.ValidationError
:
57 js_v(uuid
, id_schema2
)
59 except js_e
.ValidationError
:
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
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:
76 _convert_datetime2str(v
)
78 def _convert_bandwidth(data
, reverse
=False):
79 '''Check the field bandwidth recursivelly and when found, it removes units and convert to number
80 It assumes that bandwidth is well formed
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
87 if type(data
) is dict:
89 if type(data
[k
]) is dict or type(data
[k
]) is tuple or type(data
[k
]) is list:
90 _convert_bandwidth(data
[k
], reverse
)
91 if "bandwidth" in data
:
93 value
=str(data
["bandwidth"])
95 pos
= value
.find("bps")
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])
101 value
= int(data
["bandwidth"])
102 if value
% 1000 == 0: data
["bandwidth"]=str(value
/1000) + " Gbps"
103 else: data
["bandwidth"]=str(value
) + " Mbps"
105 print "convert_bandwidth exception for type", type(data
["bandwidth"]), " data", data
["bandwidth"]
107 if type(data
) is tuple or type(data
) is list:
109 if type(k
) is dict or type(k
) is tuple or type(k
) is list:
110 _convert_bandwidth(k
, reverse
)
112 def _convert_str2boolean(data
, items
):
113 '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean
116 'data': dictionary variable to be checked. None or empty is considered valid
117 'items': tuple of keys to convert
121 if type(data
) is dict:
122 for k
in data
.keys():
123 if type(data
[k
]) is dict or type(data
[k
]) is tuple or type(data
[k
]) is list:
124 _convert_str2boolean(data
[k
], items
)
126 if type(data
[k
]) is str:
127 if data
[k
]=="false" or data
[k
]=="False": data
[k
]=False
128 elif data
[k
]=="true" or data
[k
]=="True": data
[k
]=True
129 if type(data
) is tuple or type(data
) is list:
131 if type(k
) is dict or type(k
) is tuple or type(k
) is list:
132 _convert_str2boolean(k
, items
)
134 class db_base_Exception(Exception):
135 '''Common Exception for all database exceptions'''
137 def __init__(self
, message
, http_code
=HTTP_Bad_Request
):
138 Exception.__init
__(self
, message
)
139 self
.http_code
= http_code
142 tables_with_created_field
=()
144 def __init__(self
, host
=None, user
=None, passwd
=None, database
=None, log_name
='db', log_level
=None):
148 self
.database
= database
150 self
.log_level
=log_level
151 self
.logger
= logging
.getLogger(log_name
)
153 self
.logger
.setLevel( getattr(logging
, log_level
) )
155 def connect(self
, host
=None, user
=None, passwd
=None, database
=None):
156 '''Connect to specific data base.
157 The first time a valid host, user, passwd and database must be provided,
158 Following calls can skip this parameters
161 if host
: self
.host
= host
162 if user
: self
.user
= user
163 if passwd
: self
.passwd
= passwd
164 if database
: self
.database
= database
166 self
.con
= mdb
.connect(self
.host
, self
.user
, self
.passwd
, self
.database
)
167 print "DB: connected to %s@%s -> %s" % (self
.user
, self
.host
, self
.database
)
169 raise db_base_Exception("Cannot connect to DB {}@{} -> {} Error {}: {}".format(self
.user
, self
.host
, self
.database
, e
.args
[0], e
.args
[1]),
170 code
= HTTP_Internal_Server_Error
)
172 def get_db_version(self
):
173 ''' Obtain the database schema version.
174 Return: (negative, text) if error or version 0.0 where schema_version table is missing
175 (version_int, version_text) if ok
177 cmd
= "SELECT version_int,version FROM schema_version"
182 self
.cur
= self
.con
.cursor()
183 self
.logger
.debug(cmd
)
184 self
.cur
.execute(cmd
)
185 rows
= self
.cur
.fetchall()
186 highest_version_int
=0
188 for row
in rows
: #look for the latest version
189 if row
[0]>highest_version_int
:
190 highest_version_int
, highest_version
= row
[0:2]
191 return highest_version_int
, highest_version
192 except (mdb
.Error
, AttributeError) as e
:
193 #self.logger.error("get_db_version DB Exception %d: %s. Command %s",e.args[0], e.args[1], cmd)
194 self
._format
_error
(e
, tries
)
197 def disconnect(self
):
198 '''disconnect from specific data base'''
202 except mdb
.Error
as e
:
203 self
.logger
.error("while disconnecting from DB: Error %d: %s",e
.args
[0], e
.args
[1])
205 except AttributeError as e
: #self.con not defined
206 if e
[0][-5:] == "'con'":
207 self
.logger
.warn("while disconnecting from DB: Error %d: %s",e
.args
[0], e
.args
[1])
212 def _format_error(self
, e
, tries
=1, command
=None, extra
=None):
213 '''Creates a text error base on the produced exception
216 retry: in case of timeout, if reconnecting to database and retry, or raise and exception
217 cmd: database command that produce the exception
218 command: if the intention is update or delete
219 extra: extra information to add to some commands
221 HTTP error in negative, formatted error text
223 if isinstance(e
,AttributeError ):
224 raise db_base_Exception("DB Exception " + str(e
), HTTP_Internal_Server_Error
)
225 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
227 self
.logger
.warn("DB Exception '%s'. Retry", str(e
))
232 raise db_base_Exception("Database connection timeout Try Again", HTTP_Request_Timeout
)
234 fk
=e
.args
[1].find("foreign key constraint fails")
236 if command
=="update":
237 raise db_base_Exception("tenant_id '{}' not found.".format(extra
), HTTP_Not_Found
)
238 elif command
=="delete":
239 raise db_base_Exception("Resource is not free. There are {} that prevent deleting it.".format(extra
), HTTP_Conflict
)
240 de
= e
.args
[1].find("Duplicate entry")
241 fk
= e
.args
[1].find("for key")
242 uk
= e
.args
[1].find("Unknown column")
243 wc
= e
.args
[1].find("in 'where clause'")
244 fl
= e
.args
[1].find("in 'field list'")
245 #print de, fk, uk, wc,fl
247 if fk
>=0: #error 1062
248 raise db_base_Exception("Value {} already in use for {}".format(e
.args
[1][de
+15:fk
], e
.args
[1][fk
+7:]), HTTP_Conflict
)
251 raise db_base_Exception("Field {} can not be used for filtering".format(e
.args
[1][uk
+14:wc
]), HTTP_Bad_Request
)
253 raise db_base_Exception("Field {} does not exist".format(e
.args
[1][uk
+14:wc
]), HTTP_Bad_Request
)
254 raise db_base_Exception("Database internal Error {}: {}".format(e
.args
[0], e
.args
[1]), HTTP_Internal_Server_Error
)
256 def __str2db_format(self
, data
):
257 '''Convert string data to database format.
258 If data is None it returns the 'Null' text,
259 otherwise it returns the text surrounded by quotes ensuring internal quotes are escaped.
264 return json
.dumps(str(data
))
266 def __tuple2db_format_set(self
, data
):
267 '''Compose the needed text for a SQL SET, parameter 'data' is a pair tuple (A,B),
268 and it returns the text 'A="B"', where A is a field of a table and B is the value
269 If B is None it returns the 'A=Null' text, without surrounding Null by quotes
270 If B is not None it returns the text "A='B'" or 'A="B"' where B is surrounded by quotes,
271 and it ensures internal quotes of B are escaped.
274 return str(data
[0]) + "=Null"
276 return str(data
[0]) + '=' + json
.dumps(str(data
[1]))
278 def __tuple2db_format_where(self
, data
):
279 '''Compose the needed text for a SQL WHERE, parameter 'data' is a pair tuple (A,B),
280 and it returns the text 'A="B"', where A is a field of a table and B is the value
281 If B is None it returns the 'A is Null' text, without surrounding Null by quotes
282 If B is not None it returns the text "A='B'" or 'A="B"' where B is surrounded by quotes,
283 and it ensures internal quotes of B are escaped.
286 return str(data
[0]) + " is Null"
288 # if type(data[1]) is tuple: #this can only happen in a WHERE_OR clause
292 # text.append(str(data[0]) + " is Null")
296 # text.append( str(data[0]) + "='" + out + "'" )
297 # elif '"' not in out:
298 # text.append( str(data[0]) + '="' + out + '"' )
300 # text.append( str(data[0]) + '=' + json.dumps(out) )
301 # return " OR ".join(text)
304 return str(data
[0]) + '=' + json
.dumps(out
)
306 def __tuple2db_format_where_not(self
, data
):
307 '''Compose the needed text for a SQL WHERE(not). parameter 'data' is a pair tuple (A,B),
308 and it returns the text 'A<>"B"', where A is a field of a table and B is the value
309 If B is None it returns the 'A is not Null' text, without surrounding Null by quotes
310 If B is not None it returns the text "A<>'B'" or 'A<>"B"' where B is surrounded by quotes,
311 and it ensures internal quotes of B are escaped.
314 return str(data
[0]) + " is not Null"
316 return str(data
[0]) + '<>' + json
.dumps(out
)
318 def __remove_quotes(self
, data
):
319 '''remove single quotes ' of any string content of data dictionary'''
320 for k
,v
in data
.items():
323 data
[k
] = data
[k
].replace("'","_")
325 def _update_rows(self
, table
, UPDATE
, WHERE
, modified_time
=0):
326 ''' Update one or several rows into a table.
328 UPDATE: dictionary with the key: value to change
329 table: table where to update
330 WHERE: dictionary of elements to update
331 Return: the number of updated rows, exception if error
334 values
= ",".join(map(self
.__tuple
2db
_format
_set
, UPDATE
.iteritems() ))
336 values
+= ",modified_at={:f}".format(modified_time
)
337 cmd
= "UPDATE " + table
+" SET " + values
+\
338 " WHERE " + " and ".join(map(self
.__tuple
2db
_format
_where
, WHERE
.iteritems() ))
339 self
.logger
.debug(cmd
)
340 self
.cur
.execute(cmd
)
341 return self
.cur
.rowcount
343 def _new_row_internal(self
, table
, INSERT
, add_uuid
=False, root_uuid
=None, created_time
=0):
344 ''' Add one row into a table. It DOES NOT begin or end the transaction, so self.con.cursor must be created
346 INSERT: dictionary with the key:value to insert
347 table: table where to insert
348 add_uuid: if True, it will create an uuid key entry at INSERT if not provided
349 created_time: time to add to the created_time column
350 It checks presence of uuid and add one automatically otherwise
355 #create uuid if not provided
356 if 'uuid' not in INSERT
:
357 uuid
= INSERT
['uuid'] = str(myUuid
.uuid1()) # create_uuid
359 uuid
= str(INSERT
['uuid'])
363 #defining root_uuid if not provided
364 if root_uuid
is None:
367 created_at
= created_time
369 created_at
=time
.time()
371 cmd
= "INSERT INTO uuids (uuid, root_uuid, used_at, created_at) VALUES ('{:s}','{:s}','{:s}', {:f})".format(uuid
, root_uuid
, table
, created_at
)
372 self
.logger
.debug(cmd
)
373 self
.cur
.execute(cmd
)
375 cmd
= "INSERT INTO " + table
+" SET " + \
376 ",".join(map(self
.__tuple
2db
_format
_set
, INSERT
.iteritems() ))
378 cmd
+= ",created_at=%f" % created_time
379 self
.logger
.debug(cmd
)
380 self
.cur
.execute(cmd
)
384 def _get_rows(self
,table
,uuid
):
385 cmd
= "SELECT * FROM {} WHERE uuid='{}'".format(str(table
), str(uuid
))
386 self
.logger
.debug(cmd
)
387 self
.cur
.execute(cmd
)
388 rows
= self
.cur
.fetchall()
391 def new_row(self
, table
, INSERT
, add_uuid
=False, created_time
=0):
392 ''' Add one row into a table.
394 INSERT: dictionary with the key: value to insert
395 table: table where to insert
396 tenant_id: only useful for logs. If provided, logs will use this tenant_id
397 add_uuid: if True, it will create an uuid key entry at INSERT if not provided
398 It checks presence of uuid and add one automatically otherwise
399 Return: (result, uuid) where result can be 0 if error, or 1 if ok
401 if table
in self
.tables_with_created_field
and created_time
==0:
402 created_time
=time
.time()
407 self
.cur
= self
.con
.cursor()
408 return self
._new
_row
_internal
(table
, INSERT
, add_uuid
, None, created_time
)
410 except (mdb
.Error
, AttributeError) as e
:
411 self
._format
_error
(e
, tries
)
414 def update_rows(self
, table
, UPDATE
, WHERE
, modified_time
=0):
415 ''' Update one or several rows into a table.
417 UPDATE: dictionary with the key: value to change
418 table: table where to update
419 WHERE: dictionary of elements to update
420 Return: (result, descriptive text) where result indicates the number of updated files
422 if table
in self
.tables_with_created_field
and modified_time
==0:
423 modified_time
=time
.time()
428 self
.cur
= self
.con
.cursor()
429 return self
._update
_rows
(table
, UPDATE
, WHERE
)
431 except (mdb
.Error
, AttributeError) as e
:
432 self
._format
_error
(e
, tries
)
435 def delete_row_by_id(self
, table
, uuid
):
441 self
.cur
= self
.con
.cursor()
442 cmd
= "DELETE FROM {} WHERE uuid = '{}'".format(table
, uuid
)
443 self
.logger
.debug(cmd
)
444 self
.cur
.execute(cmd
)
445 deleted
= self
.cur
.rowcount
448 self
.cur
= self
.con
.cursor()
449 cmd
= "DELETE FROM uuids WHERE root_uuid = '{}'".format(uuid
)
450 self
.logger
.debug(cmd
)
451 self
.cur
.execute(cmd
)
453 except (mdb
.Error
, AttributeError) as e
:
454 self
._format
_error
(e
, tries
, "delete", "dependencies")
457 def delete_row(self
, **sql_dict
):
458 ''' Deletes rows from a table.
459 Attribute sql_dir: dictionary with the following key: value
460 'FROM': string of table name (Mandatory)
461 'WHERE': dict of key:values, translated to key=value AND ... (Optional)
462 'WHERE_NOT': dict of key:values, translated to key<>value AND ... (Optional)
463 if value is None, it is translated to key is not null
464 'LIMIT': limit of number of rows (Optional)
465 Return: the number of deleted or exception if error
468 from_
= "FROM " + str(sql_dict
['FROM'])
469 #print 'from_', from_
470 if 'WHERE' in sql_dict
and len(sql_dict
['WHERE']) > 0:
472 where_
= "WHERE " + " AND ".join(map(self
.__tuple
2db
_format
_where
, w
.iteritems()))
474 if 'WHERE_NOT' in sql_dict
and len(sql_dict
['WHERE_NOT']) > 0:
475 w
=sql_dict
['WHERE_NOT']
476 where_2
= " AND ".join(map(self
.__tuple
2db
_format
_where
_not
, w
.iteritems()))
477 if len(where_
)==0: where_
= "WHERE " + where_2
478 else: where_
= where_
+ " AND " + where_2
479 #print 'where_', where_
480 limit_
= "LIMIT " + str(sql_dict
['LIMIT']) if 'LIMIT' in sql_dict
else ""
481 #print 'limit_', limit_
482 cmd
= " ".join( ("DELETE", from_
, where_
, limit_
) )
487 self
.cur
= self
.con
.cursor()
488 self
.logger
.debug(cmd
)
489 self
.cur
.execute(cmd
)
490 deleted
= self
.cur
.rowcount
492 except (mdb
.Error
, AttributeError) as e
:
493 self
._format
_error
(e
, tries
)
496 def get_rows_by_id(self
, table
, uuid
):
497 '''get row from a table based on uuid'''
502 self
.cur
= self
.con
.cursor(mdb
.cursors
.DictCursor
)
503 cmd
="SELECT * FROM {} where uuid='{}'".format(str(table
), str(uuid
))
504 self
.logger
.debug(cmd
)
505 self
.cur
.execute(cmd
)
506 rows
= self
.cur
.fetchall()
508 except (mdb
.Error
, AttributeError) as e
:
509 self
._format
_error
(e
, tries
)
512 def get_rows(self
, **sql_dict
):
513 ''' Obtain rows from a table.
514 Attribute sql_dir: dictionary with the following key: value
515 'SELECT': list or tuple of fields to retrieve) (by default all)
516 'FROM': string of table name (Mandatory)
517 'WHERE': dict of key:values, translated to key=value (key is null) AND ... (Optional)
518 'WHERE_NOT': dict of key:values, translated to key<>value (key is not null) AND ... (Optional)
519 'WHERE_OR': dict of key:values, translated to key=value OR ... (Optional)
520 'WHERE_AND_OR: str 'AND' or 'OR'(by default) mark the priority to 'WHERE AND (WHERE_OR)' or (WHERE) OR WHERE_OR' (Optional)
521 'LIMIT': limit of number of rows (Optional)
522 'ORDER_BY': list or tuple of fields to order
523 Return: a list with dictionaries at each row
526 select_
= "SELECT " + ("*" if 'SELECT' not in sql_dict
else ",".join(map(str,sql_dict
['SELECT'])) )
527 #print 'select_', select_
528 from_
= "FROM " + str(sql_dict
['FROM'])
529 #print 'from_', from_
532 w
=sql_dict
.get('WHERE')
534 where_and
= " AND ".join(map(self
.__tuple
2db
_format
_where
, w
.iteritems() ))
535 w
=sql_dict
.get('WHERE_NOT')
537 if where_and
: where_and
+= " AND "
538 where_and
+= " AND ".join(map(self
.__tuple
2db
_format
_where
_not
, w
.iteritems() ) )
539 w
=sql_dict
.get('WHERE_OR')
541 where_or
= " OR ".join(map(self
.__tuple
2db
_format
_where
, w
.iteritems() ))
542 if where_and
and where_or
:
543 if sql_dict
.get("WHERE_AND_OR") == "AND":
544 where_
= "WHERE " + where_and
+ " AND (" + where_or
+ ")"
546 where_
= "WHERE (" + where_and
+ ") OR " + where_or
547 elif where_and
and not where_or
:
548 where_
= "WHERE " + where_and
549 elif not where_and
and where_or
:
550 where_
= "WHERE " + where_or
553 #print 'where_', where_
554 limit_
= "LIMIT " + str(sql_dict
['LIMIT']) if 'LIMIT' in sql_dict
else ""
555 order_
= "ORDER BY " + ",".join(map(str,sql_dict
['SELECT'])) if 'ORDER_BY' in sql_dict
else ""
557 #print 'limit_', limit_
558 cmd
= " ".join( (select_
, from_
, where_
, limit_
, order_
) )
563 self
.cur
= self
.con
.cursor(mdb
.cursors
.DictCursor
)
564 self
.logger
.debug(cmd
)
565 self
.cur
.execute(cmd
)
566 rows
= self
.cur
.fetchall()
568 except (mdb
.Error
, AttributeError) as e
:
569 self
._format
_error
(e
, tries
)
572 def get_table_by_uuid_name(self
, table
, uuid_name
, error_item_text
=None, allow_serveral
=False, WHERE_OR
={}, WHERE_AND_OR
="OR"):
573 ''' Obtain One row from a table based on name or uuid.
575 table: string of table name
576 uuid_name: name or uuid. If not uuid format is found, it is considered a name
577 allow_severeral: if False return ERROR if more than one row are founded
578 error_item_text: in case of error it identifies the 'item' name for a proper output text
579 'WHERE_OR': dict of key:values, translated to key=value OR ... (Optional)
580 'WHERE_AND_OR: str 'AND' or 'OR'(by default) mark the priority to 'WHERE AND (WHERE_OR)' or (WHERE) OR WHERE_OR' (Optional
581 Return: if allow_several==False, a dictionary with this row, or error if no item is found or more than one is found
582 if allow_several==True, a list of dictionaries with the row or rows, error if no item is found
585 if error_item_text
==None:
586 error_item_text
= table
587 what
= 'uuid' if af
.check_valid_uuid(uuid_name
) else 'name'
588 cmd
= " SELECT * FROM {} WHERE {}='{}'".format(table
, what
, uuid_name
)
590 where_or
= " OR ".join(map(self
.__tuple
2db
_format
_where
, WHERE_OR
.iteritems() ))
591 if WHERE_AND_OR
== "AND":
592 cmd
+= " AND (" + where_or
+ ")"
594 cmd
+= " OR " + where_or
601 self
.cur
= self
.con
.cursor(mdb
.cursors
.DictCursor
)
602 self
.logger
.debug(cmd
)
603 self
.cur
.execute(cmd
)
604 number
= self
.cur
.rowcount
606 return -HTTP_Not_Found
, "No %s found with %s '%s'" %(error_item_text
, what
, uuid_name
)
607 elif number
>1 and not allow_serveral
:
608 return -HTTP_Bad_Request
, "More than one %s found with %s '%s'" %(error_item_text
, what
, uuid_name
)
610 rows
= self
.cur
.fetchall()
612 rows
= self
.cur
.fetchone()
614 except (mdb
.Error
, AttributeError) as e
:
615 self
._format
_error
(e
, tries
)
618 def get_uuid(self
, uuid
):
619 '''check in the database if this uuid is already present'''
620 for retry_
in range(0,2):
623 self
.cur
= self
.con
.cursor(mdb
.cursors
.DictCursor
)
624 self
.cur
.execute("SELECT * FROM uuids where uuid='" + str(uuid
) + "'")
625 rows
= self
.cur
.fetchall()
626 return self
.cur
.rowcount
, rows
627 except (mdb
.Error
, AttributeError) as e
:
628 print "nfvo_db.get_uuid DB Exception %d: %s" % (e
.args
[0], e
.args
[1])
629 r
,c
= self
._format
_error
(e
)
630 if r
!=-HTTP_Request_Timeout
or retry_
==1: return r
,c
632 def get_uuid_from_name(self
, table
, name
):
633 '''Searchs in table the name and returns the uuid
639 self
.cur
= self
.con
.cursor(mdb
.cursors
.DictCursor
)
640 where_text
= "name='" + name
+"'"
641 self
.cur
.execute("SELECT * FROM " + table
+ " WHERE "+ where_text
)
642 rows
= self
.cur
.fetchall()
643 if self
.cur
.rowcount
==0:
644 return 0, "Name %s not found in table %s" %(name
, table
)
645 elif self
.cur
.rowcount
>1:
646 return self
.cur
.rowcount
, "More than one VNF with name %s found in table %s" %(name
, table
)
647 return self
.cur
.rowcount
, rows
[0]["uuid"]
648 except (mdb
.Error
, AttributeError) as e
:
649 self
._format
_error
(e
, tries
)