| # -*- coding: utf-8 -*- |
| # Copyright 2018 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Database interface for all calls from Chromite. |
| |
| BuildStore will be the interface which communicates between CIDB, |
| Buildbucket as the underlying databases and Chromite and other callers |
| as the clients of the data. |
| """ |
| |
| from __future__ import print_function |
| |
| import os |
| |
| from chromite.lib import buildbucket_v2 |
| from chromite.lib import cidb |
| from chromite.lib import constants |
| from chromite.lib import fake_cidb |
| |
| |
| class BuildStoreException(Exception): |
| """General exception class for this module.""" |
| |
| |
| class BuildIdentifier(object): |
| """The class maintains all the IDs corresponding to a build.""" |
| |
| def __init__(self, cidb_id=None, buildbucket_id=None): |
| """Instantiate a container class for all IDs. |
| |
| Args: |
| cidb_id: ID of the build in CIDB. |
| buildbucket_id: ID of the build in Buildbucket. |
| """ |
| self.cidb_id = cidb_id |
| self.buildbucket_id = buildbucket_id |
| |
| |
| class BuildStore(object): |
| """BuildStore class to handle all DB calls.""" |
| |
| def __init__(self, _read_from_bb=False, _write_to_bb=True, |
| _write_to_cidb=True, cidb_creds=None, for_service=None): |
| """Get an instance of the BuildStore. |
| |
| Args: |
| _read_from_bb: Specify the read source. |
| _write_to_bb: Determines whether information is written to Buildbucket. |
| _write_to_cidb: Determines whether information is written to CIDB. |
| cidb_creds: CIDB credentials for scripts running outside of cbuildbot. |
| for_service: Argument for CIDBConnection.__init__(). |
| """ |
| self._read_from_bb = _read_from_bb |
| self._write_to_bb = _write_to_bb |
| self._write_to_cidb = _write_to_cidb |
| self.cidb_creds = cidb_creds |
| self.for_service = for_service |
| self.cidb_conn = None |
| self.bb_client = None |
| self.process_id = os.getpid() |
| |
| def _IsCIDBClientMissing(self): |
| """Checks to see if CIDB client is needed and is missing. |
| |
| Returns: |
| Boolean indicating the state of CIDB client. |
| """ |
| need_for_cidb = self._write_to_cidb or not self._read_from_bb |
| cidb_is_running = self.cidb_conn is not None |
| |
| return need_for_cidb and not cidb_is_running |
| |
| def _IsBuildbucketClientMissing(self): |
| """Checks to see if Buildbucket v2 client is needed and is missing. |
| |
| Returns: |
| Boolean indicating the state of Buildbucket v2 client. |
| """ |
| need_for_bb = self._write_to_bb or self._read_from_bb |
| bb_is_running = self.bb_client is not None |
| |
| return need_for_bb and not bb_is_running |
| |
| def GetCIDBHandle(self): |
| """Retrieve cidb_conn. |
| |
| Returns: |
| self.cidb_conn if initialized. |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| |
| if self.cidb_conn: |
| return self.cidb_conn |
| else: |
| raise BuildStoreException('CIDBConnection not found.') |
| |
| def InitializeClients(self): |
| """Check if underlying clients are initialized. |
| |
| Returns: |
| A boolean indicating the client statuses. |
| """ |
| pid_mismatch = (self.process_id != os.getpid()) |
| if self._IsCIDBClientMissing() or pid_mismatch: |
| self.process_id = os.getpid() |
| if self.cidb_creds: |
| for_service = self.for_service if self.for_service else False |
| self.cidb_conn = cidb.CIDBConnection(self.cidb_creds, |
| for_service=for_service) |
| elif not cidb.CIDBConnectionFactory.IsCIDBSetup(): |
| self.cidb_conn = None |
| else: |
| self.cidb_conn = ( |
| cidb.CIDBConnectionFactory.GetCIDBConnectionForBuilder()) |
| |
| if self._IsBuildbucketClientMissing() or pid_mismatch: |
| self.bb_client = buildbucket_v2.BuildbucketV2() |
| |
| return not (self._IsCIDBClientMissing() or |
| self._IsBuildbucketClientMissing()) |
| |
| def AreClientsReady(self): |
| """A front-end function for InitializeClients().""" |
| return self.InitializeClients() |
| |
| def InsertBuild(self, |
| builder_name, |
| build_number, |
| build_config, |
| bot_hostname, |
| master_build_id=None, |
| timeout_seconds=None, |
| important=None, |
| buildbucket_id=None, |
| branch=None): |
| """Insert a build row. |
| |
| Args: |
| builder_name: buildbot builder name. |
| build_number: buildbot build number. |
| build_config: cbuildbot config of build |
| bot_hostname: hostname of bot running the build |
| master_build_id: (Optional) primary key of master build to this build. |
| timeout_seconds: (Optional) If provided, total time allocated for this |
| build. A deadline is recorded in CIDB for the current |
| build to end. |
| important: (Optional) If provided, the |important| value for this build. |
| buildbucket_id: (Optional) If provided, the |buildbucket_id| value for |
| this build. |
| branch: (Optional) Manifest branch name of this build. |
| |
| Returns: |
| build_id: incremental primary ID of the build in CIDB. |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| build_id = 0 |
| if self._write_to_cidb: |
| build_id = self.cidb_conn.InsertBuild( |
| builder_name, build_number, build_config, bot_hostname, |
| master_build_id, timeout_seconds, important, buildbucket_id, branch) |
| if self._write_to_bb: |
| buildbucket_v2.UpdateSelfCommonBuildProperties(critical=important) |
| |
| return build_id |
| |
| def GetBuildMessages(self, build_id, message_type=None, message_subtype=None): |
| """Gets build messages for particular build_id. |
| |
| Args: |
| build_id: The build to get messages for. |
| message_type: Get messages with the specific message_type (string) if |
| message_type is not None. |
| message_subtype: Get messages with the specific message_subtype (string) |
| if message_subtype is not None. |
| |
| Returns: |
| A list of build message dictionaries, where each dictionary contains |
| keys build_id, build_config, builder_name, build_number, message_type, |
| message_subtype, message_value, timestamp, board. |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if not self._read_from_bb: |
| return self.cidb_conn.GetBuildMessages(build_id, message_type, |
| message_subtype) |
| |
| def InsertBuildStage(self, |
| build_id, |
| name, |
| board=None, |
| status=constants.BUILDER_STATUS_PLANNED): |
| """Insert a build stage entry into database. |
| |
| Args: |
| build_id: primary key of the build in buildTable. |
| name: Full name of build stage. |
| board: (Optional) board name, if this is a board-specific stage. |
| status: (Optional) stage status, one of constants.BUILDER_ALL_STATUSES. |
| Default constants.BUILDER_STATUS_PLANNED. |
| |
| Returns: |
| Integer primary key of inserted stage, i.e. build_stage_id |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if self._write_to_cidb: |
| return self.cidb_conn.InsertBuildStage(build_id, name, board, status) |
| |
| def InsertFailure(self, build_stage_id, exception_type, exception_message, |
| exception_category=constants.EXCEPTION_CATEGORY_UNKNOWN, |
| outer_failure_id=None, extra_info=None): |
| """Insert a failure description into CIDB. |
| |
| Args: |
| build_stage_id: primary key, in CIDB's buildStageTable, of the stage |
| where failure occured. |
| exception_type: str name of the exception class. |
| exception_message: str description of the failure. |
| exception_category: (Optional) one of |
| constants.EXCEPTION_CATEGORY_ALL_CATEGORIES, |
| Default: 'unknown'. |
| outer_failure_id: (Optional) primary key of outer failure which contains |
| this failure. Used to store CompoundFailure |
| relationship. |
| extra_info: (Optional) extra category-specific string description giving |
| failure details. Used for programmatic triage. |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if self._write_to_cidb: |
| return self.cidb_conn.InsertFailure(build_stage_id, exception_type, |
| exception_message, exception_category, |
| outer_failure_id, extra_info) |
| |
| def FinishBuild(self, build_id, status=None, summary=None, metadata_url=None, |
| strict=True): |
| """Update the given build row, marking it as finished. |
| |
| This should be called once per build, as the last update to the build. |
| This will also mark the row's final=True. |
| |
| Args: |
| build_id: id of row to update. |
| status: Final build status, one of |
| constants.BUILDER_COMPLETED_STATUSES. |
| summary: A summary of the build (failures) collected from all slaves. |
| metadata_url: google storage url to metadata.json file for this build, |
| e.g. ('gs://chromeos-image-archive/master-paladin/' |
| 'R39-6225.0.0-rc1/metadata.json') |
| strict: If |strict| is True, can only update the build status when 'final' |
| is False. |strict| can only be False when the caller wants to change the |
| entry ignoring the 'final' value (For example, a build was marked as |
| status='aborted' and final='true', a cron job to adjust the finish_time |
| will call this method with strict=False). |
| |
| Returns: |
| The number of rows that were updated. |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if self._write_to_cidb: |
| return self.cidb_conn.FinishBuild( |
| build_id, status=status, summary=summary, metadata_url=metadata_url, |
| strict=strict) |
| |
| def FinishChildConfig(self, build_id, child_config, status=None): |
| """Marks the given child config as finished with |status|. |
| |
| This should be called before FinishBuild, on all child configs that |
| were used in a build. |
| |
| Args: |
| build_id: primary key of the build in the buildTable |
| child_config: String child_config name. |
| status: Final child_config status, one of |
| constants.BUILDER_COMPLETED_STATUSES or None |
| for default "inflight". |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if self._write_to_cidb: |
| self.cidb_conn.FinishChildConfig(build_id, child_config, status=status) |
| |
| def StartBuildStage(self, build_stage_id): |
| """Marks a build stage as inflight, in the database. |
| |
| Args: |
| build_stage_id: primary key of the build stage in buildStageTable. |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if self._write_to_cidb: |
| return self.cidb_conn.StartBuildStage(build_stage_id) |
| |
| def WaitBuildStage(self, build_stage_id): |
| """Marks a build stage as waiting, in the database. |
| |
| Args: |
| build_stage_id: primary key of the build stage in buildStageTable. |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if self._write_to_cidb: |
| return self.cidb_conn.WaitBuildStage(build_stage_id) |
| |
| def FinishBuildStage(self, build_stage_id, status): |
| """Marks a build stage as finished, in the database. |
| |
| Args: |
| build_stage_id: primary key of the build stage in buildStageTable. |
| status: one of constants.BUILDER_COMPLETED_STATUSES |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if self._write_to_cidb: |
| return self.cidb_conn.FinishBuildStage(build_stage_id, status) |
| |
| def UpdateMetadata(self, build_id, metadata): |
| """Update the given metadata row in database. |
| |
| Args: |
| build_id: CIDB id of the build to update. |
| metadata: CBuildbotMetadata instance to update with. |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| update_status = 0 |
| if self._write_to_cidb: |
| update_status = self.cidb_conn.UpdateMetadata(build_id, metadata) |
| if self._write_to_bb: |
| buildbucket_v2.UpdateBuildMetadata(metadata) |
| return update_status |
| |
| def GetBuildsFailures(self, buildbucket_ids=None): |
| """Gets the failure entries for all listed buildbucket_ids. |
| |
| Args: |
| buildbucket_ids: list of build ids of the builds to fetch failures for. |
| |
| Returns: |
| A list of failure_message_lib.StageFailure instances. This will change |
| with Buildbucket implementation. |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if not self._read_from_bb: |
| if buildbucket_ids: |
| return self.cidb_conn.GetBuildsFailures(buildbucket_ids) |
| else: |
| return [] |
| |
| def GetBuildsStages(self, build_ids=None, buildbucket_ids=None): |
| """Gets all the stages for all listed build_ids. |
| |
| Args: |
| build_ids: list of CIDB ids of the builds to fetch the stages for. |
| buildbucket_ids: list of Buildbucket IDs to query for. |
| |
| Returns: |
| A list containing, for each stage of the builds found, a dictionary with |
| keys (id, build_id, name, board, status, last_updated, start_time, |
| finish_time, final). |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if not self._read_from_bb: |
| if build_ids: |
| return self.cidb_conn.GetBuildsStages(build_ids) |
| elif buildbucket_ids: |
| return self.cidb_conn.GetBuildsStagesWithBuildbucketIds(buildbucket_ids) |
| else: |
| return [] |
| |
| def ExtendDeadline(self, build_id, timeout): |
| """Extend the deadline for the given metadata row in the database. |
| |
| Args: |
| build_id: CIDB id of the build to update. |
| timeout: new timeout value. |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if self._write_to_cidb: |
| return self.cidb_conn.ExtendDeadline(build_id, timeout) |
| |
| def GetBuildStatuses(self, buildbucket_ids=None, build_ids=None): |
| """Retrieve the build statuses of list of builds. |
| |
| The two arguments are to be mutually exclusive. If both are provided, |
| an error will be raised. If both are absent, an empty list will be returned. |
| |
| Args: |
| buildbucket_ids: list of buildbucket_id's to query. |
| build_ids: list of CIDB id's to query. |
| |
| Returns: |
| A list of Dictionaries with keys (id, build_config, start_time, |
| finish_time, status, platform_version, full_version, |
| milestone_version, important). |
| """ |
| if not self.InitializeClients(): |
| raise BuildStoreException('BuildStore clients could not be initialized.') |
| if not self._read_from_bb: |
| if buildbucket_ids and build_ids: |
| raise BuildStoreException('GetBuildStatuses: Cannot process both ' |
| 'buildbucket_ids and build_ids.') |
| if buildbucket_ids: |
| return self.cidb_conn.GetBuildStatusesWithBuildbucketIds( |
| buildbucket_ids) |
| elif build_ids: |
| return self.cidb_conn.GetBuildStatuses(build_ids) |
| else: |
| return [] |
| |
| |
| class FakeBuildStore(object): |
| """Fake BuildStore class to be used only in unittests.""" |
| |
| def __init__(self, fake_cidb_conn=None): |
| super(FakeBuildStore, self).__init__() |
| if fake_cidb_conn: |
| self.fake_cidb = fake_cidb_conn |
| else: |
| self.fake_cidb = fake_cidb.FakeCIDBConnection() |
| |
| def InitializeClients(self): |
| return True |
| |
| def AreClientsReady(self): |
| return True |
| |
| def GetCIDBHandle(self): |
| return self.fake_cidb |
| |
| def InsertBuild(self, |
| builder_name, |
| build_number, |
| build_config, |
| bot_hostname, |
| master_build_id=None, |
| timeout_seconds=None, |
| status=constants.BUILDER_STATUS_PASSED, |
| important=None, |
| buildbucket_id=None, |
| milestone_version=None, |
| platform_version=None, |
| start_time=None, |
| build_type=None, |
| branch=None): |
| build_id = self.fake_cidb.InsertBuild( |
| builder_name, build_number, build_config, bot_hostname, master_build_id, |
| timeout_seconds, status, important, buildbucket_id, milestone_version, |
| platform_version, start_time, build_type, branch) |
| return build_id |
| |
| def InsertBuildStage(self, |
| build_id, |
| name, |
| board=None, |
| status=constants.BUILDER_STATUS_PLANNED): |
| build_stage_id = self.fake_cidb.InsertBuildStage(build_id, name, board, |
| status) |
| return build_stage_id |
| |
| def InsertFailure(self, build_stage_id, exception_type, exception_message, |
| exception_category=constants.EXCEPTION_CATEGORY_UNKNOWN, |
| outer_failure_id=None, extra_info=None): |
| return self.fake_cidb.InsertFailure(build_stage_id, exception_type, |
| exception_message, exception_category, |
| outer_failure_id, extra_info) |
| |
| #pylint: disable=unused-argument |
| def FinishBuild(self, build_id, status=None, summary=None, metadata_url=None, |
| strict=True): |
| return |
| #pylint: enable=unused-argument |
| |
| def StartBuildStage(self, build_stage_id): |
| return build_stage_id |
| |
| def WaitBuildStage(self, build_stage_id): |
| return build_stage_id |
| |
| #pylint: disable=unused-argument |
| def FinishBuildStage(self, build_stage_id, status): |
| return build_stage_id |
| #pylint: enable=unused-argument |
| |
| def UpdateMetadata(self, build_id, metadata): #pylint: disable=unused-argument |
| return |
| |
| def GetBuildsFailures(self, buildbucket_ids=None): |
| if buildbucket_ids: |
| return self.fake_cidb.GetBuildsFailures(buildbucket_ids) |
| else: |
| return [] |
| |
| def GetBuildsStages(self, buildbucket_ids=None, build_ids=None): |
| if buildbucket_ids: |
| return self.fake_cidb.GetBuildsStagesWithBuildbucketIds(buildbucket_ids) |
| elif build_ids: |
| return self.fake_cidb.GetBuildsStages(build_ids) |
| else: |
| return [] |
| |
| def ExtendDeadline(self, build_id, timeout): #pylint: disable=unused-argument |
| return |
| |
| def GetBuildStatuses(self, buildbucket_ids=None, build_ids=None): |
| if buildbucket_ids: |
| return self.fake_cidb.GetBuildStatusesWithBuildbucketIds(buildbucket_ids) |
| elif build_ids: |
| return self.fake_cidb.GetBuildStatuses(build_ids) |
| else: |
| return [] |