1 # Copyright 2014 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Continuous Integration Database Library."""
7 from __future__ import print_function
16 import sqlalchemy.interfaces
17 from sqlalchemy import MetaData
20 'Unable to import sqlalchemy. Please install this package by running '
21 '`sudo apt-get install python-sqlalchemy` or similar.')
23 from chromite.cbuildbot import constants
24 from chromite.lib import retry_util
26 CIDB_MIGRATIONS_DIR = os.path.join(constants.CHROMITE_DIR, 'cidb',
29 _RETRYABLE_OPERATIONAL_ERROR_CODES = (
30 2006, # Error code 2006 'MySQL server has gone away' indicates that
31 # the connection used was closed or dropped
32 2013, # 'Lost connection to MySQL server during query'
33 # TODO(akeshet): consider only retrying UPDATE queries against
34 # this error code, not INSERT queries, since we don't know
35 # whether the query completed before or after the connection
37 2026, # 'SSL connection error: unknown error number'
41 def _IsRetryableException(e):
42 """Determine whether a query should be retried based on exception.
44 Intended for use as a handler for retry_util.
47 e: The exception to be filtered.
50 True if the query should be retried, False otherwise.
52 if isinstance(e, sqlalchemy.exc.OperationalError):
53 error_code = e.orig.args[0]
54 if error_code in _RETRYABLE_OPERATIONAL_ERROR_CODES:
60 class DBException(Exception):
61 """General exception class for this module."""
64 class UnsupportedMethodException(DBException):
65 """Raised when a call is made that the database does not support."""
68 def minimum_schema(min_version):
69 """Generate a decorator to specify a minimum schema version for a method.
71 This decorator should be applied only to instance methods of
72 SchemaVersionedMySQLConnection objects.
76 def wrapper(self, *args, **kwargs):
77 if self.schema_version < min_version:
78 raise UnsupportedMethodException()
79 return f(self, *args, **kwargs)
84 class StrictModeListener(sqlalchemy.interfaces.PoolListener):
85 """This listener ensures that STRICT_ALL_TABLES for all connections."""
86 # pylint: disable-msg=W0613
87 def connect(self, dbapi_con, *args, **kwargs):
88 cur = dbapi_con.cursor()
89 cur.execute("SET SESSION sql_mode='STRICT_ALL_TABLES'")
93 class SchemaVersionedMySQLConnection(object):
94 """Connection to a database that is aware of its schema version."""
96 SCHEMA_VERSION_TABLE_NAME = 'schemaVersionTable'
97 SCHEMA_VERSION_COL = 'schemaVersion'
99 def __init__(self, db_name, db_migrations_dir, db_credentials_dir):
100 """SchemaVersionedMySQLConnection constructor.
103 db_name: Name of the database to connect to.
104 db_migrations_dir: Absolute path to directory of migration scripts
106 db_credentials_dir: Absolute path to directory containing connection
107 information to the database. Specifically, this
108 directory should contain files names user.txt,
109 password.txt, host.txt, client-cert.pem,
110 client-key.pem, and server-ca.pem
112 # None, or a sqlalchemy.MetaData instance
115 # pid of process on which _engine was created
116 self._engine_pid = None
120 self.db_migrations_dir = db_migrations_dir
121 self.db_credentials_dir = db_credentials_dir
122 self.db_name = db_name
124 with open(os.path.join(db_credentials_dir, 'password.txt')) as f:
125 password = f.read().strip()
126 with open(os.path.join(db_credentials_dir, 'host.txt')) as f:
127 host = f.read().strip()
128 with open(os.path.join(db_credentials_dir, 'user.txt')) as f:
129 user = f.read().strip()
131 cert = os.path.join(db_credentials_dir, 'client-cert.pem')
132 key = os.path.join(db_credentials_dir, 'client-key.pem')
133 ca = os.path.join(db_credentials_dir, 'server-ca.pem')
134 self._ssl_args = {'ssl': {'cert': cert, 'key': key, 'ca': ca}}
136 connect_url = sqlalchemy.engine.url.URL('mysql', username=user,
140 # Create a temporary engine to connect to the mysql instance, and check if
141 # a database named |db_name| exists. If not, create one. We use a temporary
142 # engine here because the real engine will be opened with a default
143 # database name given by |db_name|.
144 temp_engine = sqlalchemy.create_engine(connect_url,
145 connect_args=self._ssl_args,
146 listeners=[StrictModeListener()])
147 databases = temp_engine.execute('SHOW DATABASES').fetchall()
148 if (db_name,) not in databases:
149 temp_engine.execute('CREATE DATABASE %s' % db_name)
150 logging.info('Created database %s', db_name)
152 temp_engine.dispose()
154 # Now create the persistent connection to the database named |db_name|.
155 # If there is a schema version table, read the current schema version
156 # from it. Otherwise, assume schema_version 0.
157 self._connect_url = sqlalchemy.engine.url.URL('mysql', username=user,
159 host=host, database=db_name)
161 self.schema_version = self.QuerySchemaVersion()
163 logging.info('Created a SchemaVersionedMySQLConnection, '
164 'sqlalchemy version %s', sqlalchemy.__version__)
166 def DropDatabase(self):
167 """Delete all data and tables from database, and drop database.
169 Use with caution. All data in database will be deleted. Invalidates
170 this database connection instance.
173 self._GetEngine().execute('DROP DATABASE %s' % self.db_name)
174 self._InvalidateEngine()
176 def QuerySchemaVersion(self):
177 """Query the database for its current schema version number.
180 The current schema version from the database's schema version table,
181 as an integer, or 0 if the table is empty or nonexistent.
183 tables = self._GetEngine().execute('SHOW TABLES').fetchall()
184 if (self.SCHEMA_VERSION_TABLE_NAME,) in tables:
185 r = self._GetEngine().execute('SELECT MAX(%s) from %s' %
186 (self.SCHEMA_VERSION_COL, self.SCHEMA_VERSION_TABLE_NAME))
187 return r.fetchone()[0] or 0
191 def _GetMigrationScripts(self):
192 """Look for migration scripts and return their versions and paths."
195 A list of (schema_version, script_path) tuples of the migration
196 scripts for this database, sorted in ascending schema_version order.
198 # Look for migration script files in the migration script directory,
199 # with names of the form [number]*.sql, and sort these by number.
200 migration_scripts = glob.glob(os.path.join(self.db_migrations_dir, '*.sql'))
202 for script in migration_scripts:
203 match = re.match(r'([0-9]*).*', os.path.basename(script))
205 migrations.append((int(match.group(1)), script))
210 def ApplySchemaMigrations(self, maxVersion=None):
211 """Apply pending migration scripts to database, in order.
214 maxVersion: The highest version migration script to apply. If
215 unspecified, all migrations found will be applied.
217 migrations = self._GetMigrationScripts()
219 # Execute the migration scripts in order, asserting that each one
220 # updates the schema version to the expected number. If maxVersion
221 # is specified stop early.
222 for (number, script) in migrations:
223 if maxVersion is not None and number > maxVersion:
226 if number > self.schema_version:
227 # Invalidate self._meta, then run script and ensure that schema
228 # version was increased.
230 logging.info('Running migration script %s', script)
231 self.RunQueryScript(script)
232 self.schema_version = self.QuerySchemaVersion()
233 if self.schema_version != number:
234 raise DBException('Migration script %s did not update '
235 'schema version to %s as expected. ' % (number,
238 def RunQueryScript(self, script_path):
239 """Run a .sql script file located at |script_path| on the database."""
240 with open(script_path, 'r') as f:
242 queries = [q.strip() for q in script.split(';') if q.strip()]
244 self._GetEngine().execute(q)
246 def _ReflectToMetadata(self):
247 """Use sqlalchemy reflection to construct MetaData model of database.
249 If self._meta is already populated, this does nothing.
251 if self._meta is not None:
253 self._meta = MetaData()
254 self._meta.reflect(bind=self._GetEngine())
256 def _Insert(self, table, values):
257 """Create and execute a one-row INSERT query.
260 table: Table name to insert to.
261 values: Dictionary of column values to insert.
264 Integer primary key of the inserted row.
266 self._ReflectToMetadata()
267 ins = self._meta.tables[table].insert().values(values)
268 r = self._Execute(ins)
269 return r.inserted_primary_key[0]
271 def _InsertMany(self, table, values):
272 """Create and execute an multi-row INSERT query.
275 table: Table name to insert to.
276 values: A list of value dictionaries to insert multiple rows.
279 The number of inserted rows.
281 # sqlalchemy 0.7 and prior has a bug in which it does not always
282 # correctly unpack a list of rows to multi-insert if the list contains
285 self._Insert(table, values[0])
288 self._ReflectToMetadata()
289 ins = self._meta.tables[table].insert().values(values)
290 r = self._Execute(ins)
293 def _GetPrimaryKey(self, table):
294 """Gets the primary key column of |table|.
296 This function requires that the given table have a 1-column promary key.
299 table: Name of table to primary key for.
302 A sqlalchemy.sql.schema.Column representing the primary key column.
305 DBException if the table does not have a single column primary key.
307 self._ReflectToMetadata()
308 t = self._meta.tables[table]
310 # TODO(akeshet): between sqlalchemy 0.7 and 0.8, a breaking change was
311 # made to how t.columns and t.primary_key are stored, and in sqlalchemy
312 # 0.7 t.columns does not have a .values() method. Hence this clumsy way
313 # of extracting the primary key column. Currently, our builders have 0.7
314 # installed. Once we drop support for 0.7, this code can be simply replaced
316 # key_columns = t.primary_key.columns.values()
317 col_names = t.columns.keys()
318 cols = [t.columns[n] for n in col_names]
319 key_columns = [c for c in cols if c.primary_key]
321 if len(key_columns) != 1:
322 raise DBException('Table %s does not have a 1-column primary '
324 return key_columns[0]
326 def _Update(self, table, row_id, values):
327 """Create and execute an UPDATE query by primary key.
330 table: Table name to update.
331 row_id: Primary key value of row to update.
332 values: Dictionary of column values to update.
335 The number of rows that were updated (0 or 1).
337 self._ReflectToMetadata()
338 primary_key = self._GetPrimaryKey(table)
339 upd = self._meta.tables[table].update().where(primary_key==row_id
341 r = self._Execute(upd)
344 def _UpdateWhere(self, table, where, values):
345 """Create and execute an update query with a custom where clause.
348 table: Table name to update.
349 where: Raw SQL for the where clause, in string form, e.g.
350 'build_id = 1 and board = "tomato"'
351 values: dictionary of column values to update.
354 The number of rows that were updated.
356 self._ReflectToMetadata()
357 upd = self._meta.tables[table].update().where(where)
358 r = self._Execute(upd, values)
361 def _Select(self, table, row_id, columns):
362 """Create and execute a one-row one-table SELECT query by primary key.
365 table: Table name to select from.
366 row_id: Primary key value of row to select.
367 columns: List of column names to select.
370 A column name to column value dict for the row found, if a row was found.
373 self._ReflectToMetadata()
374 primary_key = self._GetPrimaryKey(table)
375 table_m = self._meta.tables[table]
376 columns_m = [table_m.c[col_name] for col_name in columns]
377 sel = sqlalchemy.sql.select(columns_m).where(primary_key==row_id)
378 r = self._Execute(sel).fetchall()
380 assert len(r) == 1, 'Query by primary key returned more than 1 row.'
381 return dict(zip(columns, r[0]))
385 def _SelectWhere(self, table, where, columns):
386 """Create and execute a one-table SELECT query with a custom where clause.
389 table: Table name to update.
390 where: Raw SQL for the where clause, in string form, e.g.
391 'build_id = 1 and board = "tomato"'
392 columns: List of column names to select.
395 A list of column name to column value dictionaries each representing
396 a row that was selected.
398 self._ReflectToMetadata()
399 table_m = self._meta.tables[table]
400 columns_m = [table_m.c[col_name] for col_name in columns]
401 sel = sqlalchemy.sql.select(columns_m).where(where)
402 r = self._Execute(sel)
403 return [dict(zip(columns, values)) for values in r.fetchall()]
405 def _Execute(self, query, *args, **kwargs):
406 """Execute a query using engine, with retires.
408 This method wraps execution of a query in retries that create a new
409 engine in case the engine's connection has been dropped.
412 query: Query to execute, of type string, or sqlalchemy.Executible,
413 or other sqlalchemy-executible statement (see sqlalchemy
415 *args: Additional args passed along to .execute(...)
416 **kwargs: Additional args passed along to .execute(...)
419 The result of .execute(...)
421 f = lambda: self._GetEngine().execute(query, *args, **kwargs)
422 return retry_util.GenericRetry(
423 handler=_IsRetryableException,
428 def _GetEngine(self):
429 """Get the sqlalchemy engine for this process.
431 This method creates a new sqlalchemy engine if necessary, and
432 returns an engine that is unique to this process.
435 An sqlalchemy.engine instance for this database.
438 if pid == self._engine_pid and self._engine:
441 e = sqlalchemy.create_engine(self._connect_url,
442 connect_args=self._ssl_args,
443 listeners=[StrictModeListener()])
445 self._engine_pid = pid
446 logging.info('Created cidb engine %s@%s for pid %s', e.url.username,
450 def _InvalidateEngine(self):
451 """Dispose of an sqlalchemy engine."""
454 if pid == self._engine_pid and self._engine:
455 self._engine.dispose()
461 class CIDBConnection(SchemaVersionedMySQLConnection):
462 """Connection to a Continuous Integration database."""
463 def __init__(self, db_credentials_dir):
464 super(CIDBConnection, self).__init__('cidb', CIDB_MIGRATIONS_DIR,
468 def InsertBuild(self, builder_name, waterfall, build_number,
469 build_config, bot_hostname, master_build_id=None):
470 """Insert a build row.
473 builder_name: buildbot builder name.
474 waterfall: buildbot waterfall name.
475 build_number: buildbot build number.
476 build_config: cbuildbot config of build
477 bot_hostname: hostname of bot running the build
478 master_build_id: (Optional) primary key of master build to this build.
480 return self._Insert('buildTable', {'builder_name': builder_name,
481 'buildbot_generation':
482 constants.BUILDBOT_GENERATION,
483 'waterfall': waterfall,
484 'build_number': build_number,
485 'build_config' : build_config,
486 'bot_hostname': bot_hostname,
488 sqlalchemy.func.current_timestamp(),
489 'master_build_id' : master_build_id}
493 def InsertCLActions(self, build_id, cl_actions):
494 """Insert a list of |cl_actions|.
496 If |cl_actions| is empty, this function does nothing.
499 build_id: primary key of build that performed these actions.
500 cl_actions: A list of cl_action tuples.
503 Number of actions inserted.
509 # TODO(akeshet): Refactor to use either cl action tuples out of the
510 # metadata dict (as now) OR CLActionTuple objects.
511 for cl_action in cl_actions:
512 change_source = 'internal' if cl_action[0]['internal'] else 'external'
513 change_number = cl_action[0]['gerrit_number']
514 patch_number = cl_action[0]['patch_number']
515 action = cl_action[1]
516 reason = cl_action[3]
518 'build_id' : build_id,
519 'change_source' : change_source,
520 'change_number': change_number,
521 'patch_number' : patch_number,
525 return self._InsertMany('clActionTable', values)
528 def InsertBuildStage(self, build_id, stage_name, board, status,
529 log_url, duration_seconds, summary):
530 """Insert a build stage into buildStageTable.
533 build_id: id of responsible build
534 stage_name: name of stage
535 board: board that stage ran for
536 status: 'pass' or 'fail'
537 log_url: URL of stage log
538 duration_seconds: run time of stage, in seconds
539 summary: summary message of stage
542 Primary key of inserted stage.
544 return self._Insert('buildStageTable',
545 {'build_id': build_id,
550 'duration_seconds': duration_seconds,
554 def InsertBuildStages(self, stages):
555 """For testing only. Insert multiple build stages into buildStageTable.
557 This method allows integration tests to more quickly populate build
558 stages into the database, from test data. Normal builder operations are
559 expected to insert build stage rows one at a time, using InsertBuildStage.
562 stages: A list of dictionaries, each dictionary containing keys
563 build_id, name, board, status, log_url, duration_seconds, and
567 The number of build stage rows inserted.
571 return self._InsertMany('buildStageTable',
575 def InsertBoardPerBuild(self, build_id, board):
576 """Inserts a board-per-build entry into database.
579 build_id: primary key of the build in the buildTable
580 board: String board name.
582 self._Insert('boardPerBuildTable', {'build_id': build_id,
586 def InsertChildConfigPerBuild(self, build_id, child_config):
587 """Insert a child-config-per-build entry into database.
590 build_id: primary key of the build in the buildTable
591 child_config: String child_config name.
593 self._Insert('childConfigPerBuildTable', {'build_id': build_id,
594 'child_config': child_config})
597 def UpdateMetadata(self, build_id, metadata):
598 """Update the given metadata row in database.
601 build_id: id of row to update.
602 metadata: CBuildbotMetadata instance to update with.
605 The number of build rows that were updated (0 or 1).
607 d = metadata.GetDict()
608 versions = d.get('version') or {}
609 return self._Update('buildTable', build_id,
610 {'chrome_version': versions.get('chrome'),
611 'milestone_version': versions.get('milestone'),
612 'platform_version': versions.get('platform'),
613 'full_version': versions.get('full'),
614 'sdk_version': d.get('sdk-versions'),
615 'toolchain_url': d.get('toolchain-url'),
616 'build_type': d.get('build_type')})
619 def UpdateBoardPerBuildMetadata(self, build_id, board, board_metadata):
620 """Update the given board-per-build metadata.
623 build_id: id of the build
624 board: board to update
625 board_metadata: per-board metadata dict for this board
628 'main_firmware_version': board_metadata.get('main-firmware-version'),
629 'ec_firmware_version': board_metadata.get('ec-firmware-version')
631 return self._UpdateWhere('boardPerBuildTable',
632 'build_id = %s and board = "%s"' % (build_id, board),
637 def FinishBuild(self, build_id, status=None, status_pickle=None,
639 """Update the given build row, marking it as finished.
641 This should be called once per build, as the last update to the build.
642 This will also mark the row's final=True.
645 build_id: id of row to update.
646 status: Final build status, one of
647 manifest_version.BuilderStatus.COMPLETED_STATUSES.
648 status_pickle: Pickled manifest_version.BuilderStatus.
649 metadata_url: google storage url to metadata.json file for this build,
650 e.g. ('gs://chromeos-image-archive/master-paladin/'
651 'R39-6225.0.0-rc1/metadata.json')
653 self._ReflectToMetadata()
654 # The current timestamp is evaluated on the database, not locally.
655 current_timestamp = sqlalchemy.func.current_timestamp()
656 self._Update('buildTable', build_id, {'finish_time' : current_timestamp,
658 'status_pickle' : status_pickle,
659 'metadata_url': metadata_url,
663 def GetBuildStatus(self, build_id):
664 """Gets the status of the build.
667 build_id: build id to fetch.
670 A dictionary with keys (id, build_config, start_time, finish_time,
671 status), or None if no build with this id was found.
673 return self._Select('buildTable', build_id,
674 ['id', 'build_config', 'start_time',
675 'finish_time', 'status'])
678 def GetSlaveStatuses(self, master_build_id):
679 """Gets the statuses of slave builders to given build.
682 master_build_id: build id of the master build to fetch the slave
686 A list containing, for each slave build (row) found, a dictionary
687 with keys (id, build_config, start_time, finish_time, status).
689 return self._SelectWhere('buildTable',
690 'master_build_id = %s' % master_build_id,
691 ['id', 'build_config', 'start_time',
692 'finish_time', 'status'])
695 def GetActionsForChange(self, change):
696 """Gets all the actions for the given change.
698 Note, this includes all patches of the given change.
701 change: A GerritChangeTuple or GerritPatchTuple specifing the
705 A list of actions, in timestamp order, each of which is a dict
706 with keys (id, build_id, action, build_config, change_number,
707 patch_number, change_source, timestamp)
709 change_number = int(change.gerrit_number)
710 change_source = 'internal' if change.internal else 'external'
711 results = self._Execute(
712 'SELECT c.id, b.id, action, build_config, change_number, '
713 'patch_number, change_source, timestamp FROM '
714 'clActionTable c JOIN buildTable b ON build_id = b.id '
715 'WHERE change_number = %s AND change_source = "%s"' % (
716 change_number, change_source)).fetchall()
717 columns = ['id', 'build_id', 'action', 'build_config', 'change_number',
718 'patch_number', 'change_source', 'timestamp']
719 return [dict(zip(columns, values)) for values in results]
722 class CIDBConnectionFactory(object):
723 """Factory class used by builders to fetch the appropriate cidb connection"""
725 # A call to one of the Setup methods below is necessary before using the
726 # GetCIDBConnectionForBuilder Factory. This ensures that unit tests do not
727 # accidentally use one of the live database instances.
729 _ConnectionIsSetup = False
730 _ConnectionType = None
731 _ConnectionCredsPath = None
735 _CONNECTION_TYPE_PROD = 'prod' # production database
736 _CONNECTION_TYPE_DEBUG = 'debug' # debug database, used by --debug builds
737 _CONNECTION_TYPE_MOCK = 'mock' # mock connection, not backed by database
738 _CONNECTION_TYPE_NONE = 'none' # explicitly no connection
739 _CONNECTION_TYPE_INV = 'invalid' # invalidated connection
743 def IsCIDBSetup(cls):
744 """Returns True iff GetCIDBConnectionForBuilder is ready to be called."""
745 return cls._ConnectionIsSetup
748 def GetCIDBConnectionType(cls):
749 """Returns the type of db connection that is set up.
752 One of ('prod', 'debug', 'mock', 'none', 'invalid', None)
754 return cls._ConnectionType
757 def InvalidateCIDBSetup(cls):
758 """Invalidate the CIDB connection factory.
760 This method may be called at any time, even after a setup method. Once
761 this is called, future calls to GetCIDBConnectionForBuilder will raise
764 cls._ConnectionType = cls._CONNECTION_TYPE_INV
765 cls._CachedCIDB = None
768 def SetupProdCidb(cls):
769 """Sets up CIDB to use the prod instance of the database.
771 May be called only once, and may not be called after any other CIDB Setup
772 method, otherwise it will raise an AssertionError.
774 assert not cls._ConnectionIsSetup, 'CIDB is already set up.'
775 assert not cls._ConnectionType == cls._CONNECTION_TYPE_MOCK, (
776 'CIDB set for mock use.')
777 cls._ConnectionType = cls._CONNECTION_TYPE_PROD
778 cls._ConnectionCredsPath = constants.CIDB_PROD_BOT_CREDS
779 cls._ConnectionIsSetup = True
783 def SetupDebugCidb(cls):
784 """Sets up CIDB to use the debug instance of the database.
786 May be called only once, and may not be called after any other CIDB Setup
787 method, otherwise it will raise an AssertionError.
789 assert not cls._ConnectionIsSetup, 'CIDB is already set up.'
790 assert not cls._ConnectionType == cls._CONNECTION_TYPE_MOCK, (
791 'CIDB set for mock use.')
792 cls._ConnectionType = cls._CONNECTION_TYPE_DEBUG
793 cls._ConnectionCredsPath = constants.CIDB_DEBUG_BOT_CREDS
794 cls._ConnectionIsSetup = True
798 def SetupMockCidb(cls, mock_cidb=None):
799 """Sets up CIDB to use a mock object. May be called more than once.
802 mock_cidb: (optional) The mock cidb object to be returned by
803 GetCIDBConnection. If not supplied, then CIDB will be
804 considered not set up, but future calls to set up a
805 non-(mock or None) connection will fail.
807 if cls._ConnectionIsSetup:
808 assert cls._ConnectionType == cls._CONNECTION_TYPE_MOCK, (
809 'A non-mock CIDB is already set up.')
810 cls._ConnectionType = cls._CONNECTION_TYPE_MOCK
812 cls._ConnectionIsSetup = True
813 cls._MockCIDB = mock_cidb
817 def SetupNoCidb(cls):
818 """Sets up CIDB to use an explicit None connection.
820 May be called more than once, or after SetupMockCidb.
822 if cls._ConnectionIsSetup:
823 assert (cls._ConnectionType == cls._CONNECTION_TYPE_MOCK or
824 cls._ConnectionType == cls._CONNECTION_TYPE_NONE) , (
825 'A non-mock CIDB is already set up.')
826 cls._ConnectionType = cls._CONNECTION_TYPE_NONE
827 cls._ConnectionIsSetup = True
831 def GetCIDBConnectionForBuilder(cls):
832 """Get a CIDBConnection.
834 A call to one of the CIDB Setup methods must have been made before calling
838 A CIDBConnection instance connected to either the prod or debug
839 instance of the database, or a mock connection, depending on which
840 Setup method was called. Returns None if CIDB has been explicitly
841 set up for that using SetupNoCidb.
843 assert cls._ConnectionIsSetup, 'CIDB has not be set up with a Setup call.'
844 assert cls._ConnectionType != cls._CONNECTION_TYPE_INV, (
845 'CIDB Connection factory has been invalidated.')
846 if cls._ConnectionType == cls._CONNECTION_TYPE_MOCK:
848 elif cls._ConnectionType == cls._CONNECTION_TYPE_NONE:
852 return cls._CachedCIDB
854 cls._CachedCIDB = CIDBConnection(cls._ConnectionCredsPath)
855 except sqlalchemy.exc.OperationalError as e:
856 logging.warn('Retrying to create a database connection, due to '
858 cls._CachedCIDB = CIDBConnection(cls._ConnectionCredsPath)
859 return cls._CachedCIDB
862 def _ClearCIDBSetup(cls):
863 """Clears the CIDB Setup state. For testing purposes only."""
864 cls._ConnectionIsSetup = False
865 cls._ConnectionType = None
866 cls._ConnectionCredsPath = None
868 cls._CachedCIDB = None