| # -*- coding: utf-8 -*- |
| # Copyright 2017 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. |
| |
| """Script to generate alerts of failing builds to Sheriff-o-Matic.""" |
| |
| from __future__ import print_function |
| |
| import collections |
| import datetime |
| import json |
| import re |
| |
| from chromite.cbuildbot import topology |
| from chromite.lib.const import waterfall |
| from chromite.lib import cidb |
| from chromite.lib import classifier |
| from chromite.lib import commandline |
| from chromite.lib import constants |
| from chromite.lib import cros_logging as logging |
| from chromite.lib import logdog |
| from chromite.lib import milo |
| from chromite.lib import parallel |
| from chromite.lib import prpc |
| from chromite.lib import som |
| from chromite.lib import tree_status |
| |
| |
| # Only display this many links per stage |
| MAX_STAGE_LINKS = 7 |
| |
| # Max history to look back in days and number of builds. |
| MAX_HISTORY_DAYS = 30 |
| MAX_CONSECUTIVE_BUILDS = 50 |
| # Last N builds to show as history. |
| MAX_LAST_N_BUILDS = 10 |
| |
| CROSLAND_VERSION_RE = re.compile(r'^\d+\.\d+\.\d+$') |
| SANITY_BUILD_CONFIG_RE = re.compile(r'.*-tot-paladin$') |
| |
| def GetParser(): |
| """Creates the argparse parser.""" |
| parser = commandline.ArgumentParser(description=__doc__) |
| parser.add_argument('--service_acct_json', type=str, action='store', |
| help='Path to service account credentials JSON file.') |
| parser.add_argument('cred_dir', action='store', |
| metavar='CIDB_CREDENTIALS_DIR', |
| help='Database credentials directory with certificates ' |
| 'and other connection information. Obtain your ' |
| 'credentials at go/cros-cidb-admin .') |
| parser.add_argument('--logdog_host', type=str, action='store', |
| help='URL of Logdog host.') |
| parser.add_argument('--milo_host', type=str, action='store', |
| help='URL of MILO host.') |
| parser.add_argument('--som_host', type=str, action='store', |
| help='Sheriff-o-Matic host to post alerts to.') |
| parser.add_argument('--som_insecure', action='store_true', default=False, |
| help='Use insecure Sheriff-o-Matic connection.') |
| parser.add_argument('--som_tree', type=str, action='store', |
| default=constants.SOM_TREE, |
| help='Sheriff-o-Matic tree to post alerts to.') |
| parser.add_argument('--output_json', type=str, action='store', |
| help='Filename to write JSON to.') |
| parser.add_argument('--json_file', type=str, action='store', |
| help='JSON file to send.') |
| parser.add_argument('--allow_experimental', action='store_true', |
| help='Include experimental builds.') |
| parser.add_argument('builds', type=str, nargs='*', action='store', |
| metavar='WATERFALL,TREE,SEVERITY|BUILD_ID,SEVERITY', |
| help='Builds to report on. eg chromeos,elm-release,1000 ' |
| 'or chromeos,master-paladin,1001 or 1234567,,1000') |
| return parser |
| |
| |
| class ObjectEncoder(json.JSONEncoder): |
| """Helper object to encode object dictionaries to JSON.""" |
| # pylint: disable=method-hidden |
| def default(self, obj): |
| return obj.__dict__ |
| |
| |
| def MapCIDBToSOMStatus(status, message_type=None, message_subtype=None): |
| """Map CIDB status to Sheriff-o-Matic display status. |
| |
| In particular, maps inflight stages to being timed out since if they're |
| being reported, the master has finished and the inflight stage has not |
| completed, likely due to the entire build timing out. |
| |
| Args: |
| status: A status string from CIDB. |
| message_type: A message type string from CIDB. |
| message_subtype: A message subtype string from CIDB. |
| |
| Returns: |
| A string suitable for displaying by Sheriff-o-Matic. |
| """ |
| STATUS_MAP = { |
| constants.BUILDER_STATUS_FAILED: 'failed', |
| constants.BUILDER_STATUS_INFLIGHT: 'timed out', |
| } |
| if status in STATUS_MAP: |
| status = STATUS_MAP[status] |
| if message_type == constants.MESSAGE_TYPE_IGNORED_REASON: |
| IGNORED_MAP = { |
| constants.MESSAGE_SUBTYPE_SELF_DESTRUCTION: 'self destructed', |
| } |
| if message_subtype in IGNORED_MAP: |
| status = IGNORED_MAP[message_subtype] |
| return status |
| |
| |
| def AddLogsLink(logdog_client, name, |
| wfall, logdog_prefix, annotation_stream, logs_links): |
| """Helper to add a Logdog link. |
| |
| Args: |
| logdog_client: logdog.LogdogClient object. |
| name: A name for the the link. |
| wfall: Waterfall for the Logdog stream |
| logdog_prefix: Logdog prefix of the stream. |
| annotation_stream: Logdog annotation for the stream. |
| logs_links: List to add to if the stream is valid. |
| """ |
| if annotation_stream and annotation_stream['name']: |
| url = logdog_client.ConstructViewerURL(wfall, |
| logdog_prefix, |
| annotation_stream['name']) |
| logs_links.append(som.Link(name, url)) |
| |
| |
| def GenerateAlertStage(build, stage, exceptions, aborted, |
| buildinfo, logdog_client): |
| """Generate alert details for a single build stage. |
| |
| Args: |
| build: Dictionary of build details from CIDB. |
| stage: Dictionary of stage details from CIDB. |
| exceptions: A list of instances of failure_message_lib.StageFailure. |
| aborted: Boolean indicated if the build was aborted. |
| buildinfo: BuildInfo build JSON file from MILO. |
| logdog_client: logdog.LogdogClient object. |
| |
| Returns: |
| som.CrosStageFailure object if stage requires alert. None otherwise. |
| """ |
| STAGE_IGNORE_STATUSES = frozenset([constants.BUILDER_STATUS_PASSED, |
| constants.BUILDER_STATUS_PLANNED, |
| constants.BUILDER_STATUS_SKIPPED]) |
| ABORTED_IGNORE_STATUSES = frozenset([constants.BUILDER_STATUS_INFLIGHT, |
| constants.BUILDER_STATUS_FORGIVEN, |
| constants.BUILDER_STATUS_WAITING]) |
| NO_LOG_RETRY_STATUSES = frozenset([constants.BUILDER_STATUS_INFLIGHT, |
| constants.BUILDER_STATUS_ABORTED]) |
| # IGNORE_EXCEPTIONS should be ignored if they're the only exception. |
| IGNORE_EXCEPTIONS = frozenset(['ImportantBuilderFailedException']) |
| # ABORTED_DISREGARD_EXCEPTIONS should cause any failures of aborted stages |
| # to be entirely disregarded. |
| ABORTED_DISREGARD_EXCEPTIONS = frozenset(['_ShutDownException']) |
| if (stage['build_id'] != build['id'] or |
| stage['status'] in STAGE_IGNORE_STATUSES): |
| return None |
| if aborted and stage['status'] in ABORTED_IGNORE_STATUSES: |
| return None |
| |
| logging.info(' stage %s (id %d): %s', stage['name'], stage['id'], |
| stage['status']) |
| logs_links = [] |
| notes = [] |
| |
| # Generate links to the logs of the stage and use them for classification. |
| if buildinfo and stage['name'] in buildinfo['steps']: |
| prefix = buildinfo['annotationStream']['prefix'] |
| annotation = buildinfo['steps'][stage['name']] |
| AddLogsLink(logdog_client, 'stdout', buildinfo['project'], |
| prefix, annotation.get('stdoutStream'), logs_links) |
| AddLogsLink(logdog_client, 'stderr', buildinfo['project'], |
| prefix, annotation.get('stderrStream'), logs_links) |
| |
| # Use the logs in an attempt to classify the failure. |
| if (annotation.get('stdoutStream') and |
| annotation['stdoutStream'].get('name')): |
| path = '%s/+/%s' % (prefix, annotation['stdoutStream']['name']) |
| try: |
| # If either the build or stage is reporting as being inflight, |
| # LogDog might still be waiting for logs so don't wait unnecesarily |
| # for them. |
| retry = (build['status'] not in NO_LOG_RETRY_STATUSES and |
| stage['status'] not in NO_LOG_RETRY_STATUSES) |
| logs = logdog_client.GetLines(buildinfo['project'], path, |
| allow_retries=retry) |
| classification = classifier.ClassifyFailure(stage['name'], logs) |
| for c in classification or []: |
| notes.append('Classification: %s' % (c)) |
| except Exception as e: |
| logging.exception('Could not classify logs: %s', e) |
| notes.append('Warning: unable to classify logs: %s' % (e)) |
| elif aborted: |
| # Aborted build with no stage logs is not worth reporting on. |
| return None |
| else: |
| notes.append('Warning: stage logs unavailable') |
| |
| # Copy the links from the buildbot build JSON. |
| stage_links = [] |
| if buildinfo: |
| if stage['status'] == constants.BUILDER_STATUS_FORGIVEN: |
| # TODO: Include these links but hide them by default in frontend. |
| pass |
| elif stage['name'] in buildinfo['steps']: |
| step = buildinfo['steps'][stage['name']] |
| stage_links = [som.Link(l['label'], l['url']) |
| for l in step.get('otherLinks', [])] |
| else: |
| steps = [s for s in buildinfo['steps'].keys() |
| if s is not None and not isinstance(s, tuple)] |
| logging.warn('Could not find stage %s in: %s', |
| stage['name'], ', '.join(steps)) |
| else: |
| notes.append('Warning: stage details unavailable') |
| |
| # Limit the number of links that will be displayed for a single stage. |
| # Let there be one extra since it doesn't make sense to have a line |
| # saying there is one more. |
| # TODO: Move this to frontend so they can be unhidden by clicking. |
| if len(stage_links) > MAX_STAGE_LINKS + 1: |
| # Insert at the beginning of the notes which come right after the links. |
| notes.insert(0, '... and %d more URLs' % (len(stage_links) - |
| MAX_STAGE_LINKS)) |
| del stage_links[MAX_STAGE_LINKS:] |
| |
| # Add all exceptions recording in CIDB as notes. |
| has_other_exceptions = False |
| has_ignore_exception = False |
| for e in exceptions: |
| if e.build_stage_id == stage['id']: |
| notes.append('%s: %s' % (e.exception_type, e.exception_message)) |
| if aborted and e.exception_type in ABORTED_DISREGARD_EXCEPTIONS: |
| # Don't generate alert if the exception indicates it should be |
| # entirely disregarded. |
| return None |
| elif e.exception_type in IGNORE_EXCEPTIONS: |
| # Ignore this exception (and stage if there aren't other exceptions). |
| has_ignore_exception = True |
| continue |
| has_other_exceptions = True |
| |
| # If there is an ignored exception and no other exceptions, treat this |
| # stage as non-failed. |
| if has_ignore_exception and not has_other_exceptions: |
| return None |
| |
| # Add the stage to the alert. |
| return som.CrosStageFailure(stage['name'], |
| MapCIDBToSOMStatus(stage['status']), |
| logs_links, stage_links, notes) |
| |
| |
| def GenerateBlameLink(old_version, new_version): |
| """Generates a link to show diffs between two versions. |
| |
| Args: |
| old_version: version string. |
| new_version: version string. |
| |
| Returns: |
| string of URL to generate blame list, or None if it cannot be generated. |
| """ |
| if (CROSLAND_VERSION_RE.match(old_version) and |
| CROSLAND_VERSION_RE.match(new_version)): |
| return 'http://go/crosland/log/%s..%s' % (old_version, new_version) |
| else: |
| return None |
| |
| |
| def GenerateCompareBuildsLink(build_ids, siblings): |
| """Return the URL to compare siblings for this build. |
| |
| Args: |
| build_ids: list of CIDB id for the builds. |
| siblings: boolean indicating whether sibling builds should be included. |
| |
| Returns: |
| The fully formed URL. |
| """ |
| params = ['buildIds=%s' % ','.join([str(b) for b in build_ids])] |
| if siblings: |
| params.append('includeSiblings=true') |
| return 'http://go/buildCompare?%s' % '&'.join(params) |
| |
| |
| def SummarizeHistory(build, db): |
| """Summarizes recent history. |
| |
| Args: |
| build: Dictionary of build details from CIDB. |
| db: cidb.CIDBConnection object. |
| |
| Returns: |
| list of string describing recent history and diff links. |
| """ |
| # Get all the recent builds of the same type. |
| now = datetime.datetime.utcnow() |
| start_date = now - datetime.timedelta(days=MAX_HISTORY_DAYS) |
| history = db.GetBuildHistory( |
| build['build_config'], MAX_CONSECUTIVE_BUILDS, start_date=start_date, |
| ending_build_id=build['id'], waterfall=build['waterfall'], |
| buildbot_generation=build['buildbot_generation']) |
| history = sorted(history, key=lambda s: s['build_number'], reverse=True) |
| |
| # Count how many times the current status happened consecutively. |
| ids = [] |
| for h in history: |
| if h['status'] != build['status']: |
| break |
| ids.append(h['id']) |
| |
| # Determine histogram of last N builds. |
| last_n, frequencies = SummarizeStatuses(history[:MAX_LAST_N_BUILDS]) |
| |
| # Generate string. |
| notes = [] |
| compare_link = '' |
| if build['status'] != constants.BUILDER_STATUS_PASSED and len(ids) > 1: |
| compare_link = ' \\[[compare](%s)\\]' % GenerateCompareBuildsLink(ids, |
| False) |
| note = 'History of %s: %d %s build(s) in a row%s' % ( |
| build['builder_name'], len(ids), |
| MapCIDBToSOMStatus(build['status']), compare_link) |
| if len(frequencies) > 1: |
| ids = [h['id'] for h in history[:MAX_LAST_N_BUILDS]] |
| note += '; Last %d builds: %s \\[[compare](%s)\\]' % ( |
| MAX_LAST_N_BUILDS, last_n, |
| GenerateCompareBuildsLink(ids, False)) |
| notes.append(note) |
| |
| # Look for transition from most recent passing build. |
| if build['status'] != constants.BUILDER_STATUS_PASSED: |
| failure = None |
| for h in history: |
| if h['status'] == constants.BUILDER_STATUS_PASSED: |
| # Generate diff link from h .. failure |
| blame_link = GenerateBlameLink(h['platform_version'], |
| failure['platform_version']) |
| if blame_link: |
| note = ('[Diff](%s) from last passed (%s:%d) to first failure (%s:%d)' |
| % (blame_link, h['builder_name'], h['build_number'], |
| failure['builder_name'], failure['build_number'])) |
| notes.append(note) |
| break |
| failure = h |
| |
| return notes |
| |
| |
| def SummarizeStatuses(statuses): |
| """Summarizes a list of statuses. |
| |
| Args: |
| statuses: A list of dictionaries of build details from CIDB. |
| |
| Returns: |
| (string describing frequency of all the statuses, |
| Counter object of status to count) |
| """ |
| frequencies = collections.Counter([s['status'] for s in statuses]) |
| return ', '.join('%d %s' % (frequencies[h], MapCIDBToSOMStatus(h)) |
| for h in frequencies), frequencies |
| |
| |
| def GenerateBuildAlert(build, stages, exceptions, messages, annotations, |
| siblings, severity, now, db, |
| logdog_client, milo_client, allow_experimental=False): |
| """Generate an alert for a single build. |
| |
| Args: |
| build: Dictionary of build details from CIDB. |
| stages: A list of dictionaries of stage details from CIDB. |
| exceptions: A list of instances of failure_message_lib.StageFailure. |
| messages: A list of build message dictionaries from CIDB. |
| annotations: A list of dictionaries of build annotations from CIDB. |
| siblings: A list of dictionaries of build details from CIDB. |
| severity: Sheriff-o-Matic severity to use for the alert. |
| now: Current datettime. |
| db: cidb.CIDBConnection object. |
| logdog_client: logdog.LogdogClient object. |
| milo_client: milo.MiloClient object. |
| allow_experimental: Boolean if non-important builds should be included. |
| |
| Returns: |
| som.Alert object if build requires alert. None otherwise. |
| """ |
| BUILD_IGNORE_STATUSES = frozenset([constants.BUILDER_STATUS_PASSED]) |
| CIDB_INDETERMINATE_STATUSES = frozenset([constants.BUILDER_STATUS_INFLIGHT, |
| constants.BUILDER_STATUS_ABORTED]) |
| if ((not allow_experimental and not build['important']) or |
| build['status'] in BUILD_IGNORE_STATUSES): |
| logging.debug(' %s:%d (id %d) skipped important %s status %s', |
| build['builder_name'], build['build_number'], build['id'], |
| build['important'], build['status']) |
| return None |
| |
| # Record any relevant build messages, keeping track if it was aborted. |
| message = (None, None) |
| aborted = build['status'] == constants.BUILDER_STATUS_ABORTED |
| for m in messages: |
| # MESSAGE_TYPE_IGNORED_REASON implies that the target of the message |
| # is stored as message_value (as a string). |
| if (m['message_type'] == constants.MESSAGE_TYPE_IGNORED_REASON and |
| str(build['id']) == m['message_value']): |
| if m['message_subtype'] == constants.MESSAGE_SUBTYPE_SELF_DESTRUCTION: |
| aborted = True |
| message = (m['message_type'], m['message_subtype']) |
| |
| logging.info(' %s:%d (id %d) %s %s', build['builder_name'], |
| build['build_number'], build['id'], build['status'], |
| '%s/%s' % message if message[0] else '') |
| |
| # Create links for details on the build. |
| dashboard_url = tree_status.ConstructDashboardURL(build['waterfall'], |
| build['builder_name'], |
| build['build_number']) |
| annotator_url = tree_status.ConstructAnnotatorURL(build.get('master_build_id', |
| build['id'])) |
| links = [ |
| som.Link('build_details', dashboard_url), |
| som.Link('goldeneye', |
| tree_status.ConstructGoldenEyeBuildDetailsURL(build['id'])), |
| som.Link('viceroy', |
| tree_status.ConstructViceroyBuildDetailsURL(build['id'])), |
| som.Link('buildbot', |
| tree_status.ConstructBuildStageURL( |
| waterfall.WATERFALL_TO_DASHBOARD[build['waterfall']], |
| build['builder_name'], build['build_number'])), |
| som.Link('annotator', annotator_url), |
| ] |
| |
| notes = SummarizeHistory(build, db) |
| if len(siblings) > 1: |
| notes.append('Siblings: %s \\[[compare](%s)\\]' % |
| (SummarizeStatuses(siblings)[0], |
| GenerateCompareBuildsLink([build['id']], True))) |
| # Link to any existing annotations, along with a link back to the annotator. |
| notes.extend([('[Annotation](%(link)s): %(failure_category)s' |
| '(%(failure_message)s) %(blame_url)s %(notes)s') % |
| dict(a, **{'link': annotator_url}) for a in annotations]) |
| |
| # If the CIDB status was indeterminate (inflight/aborted), provide link |
| # for sheriffs. |
| if build['status'] in CIDB_INDETERMINATE_STATUSES: |
| notes.append('Indeterminate CIDB status: ' |
| 'https://yaqs.googleplex.com/eng/q/5238815784697856') |
| |
| # Annotate sanity builders as such. |
| if SANITY_BUILD_CONFIG_RE.match(build['build_config']): |
| notes.append('%s is a sanity builder: ' |
| 'https://yaqs.googleplex.com/eng/q/5913965810155520' % |
| build['build_config']) |
| |
| # TODO: Gather similar failures. |
| builders = [som.AlertedBuilder(build['builder_name'], dashboard_url, |
| ToEpoch(build['finish_time'] or now), |
| build['build_number'], build['build_number'])] |
| |
| # Access the BuildInfo for per-stage links of failed stages. |
| try: |
| buildinfo = milo_client.BuildInfoGetBuildbot(build['waterfall'], |
| build['builder_name'], |
| build['build_number']) |
| except prpc.PRPCResponseException as e: |
| logging.warning('Unable to retrieve BuildInfo: %s', e) |
| buildinfo = None |
| |
| # Highlight the problematic stages. |
| alert_stages = [] |
| for stage in stages: |
| alert_stage = GenerateAlertStage(build, stage, exceptions, aborted, |
| buildinfo, logdog_client) |
| if alert_stage: |
| alert_stages.append(alert_stage) |
| |
| if (aborted or build['master_build_id'] is None) and len(alert_stages) == 0: |
| logging.debug(' %s:%d (id %d) skipped aborted and no stages', |
| build['builder_name'], build['build_number'], build['id']) |
| return None |
| |
| # Add the alert to the summary. |
| key = '%s:%s:%d' % (build['waterfall'], build['build_config'], |
| build['build_number']) |
| alert_name = '%s:%d %s' % (build['build_config'], build['build_number'], |
| MapCIDBToSOMStatus(build['status'], |
| message[0], message[1])) |
| return som.Alert(key, alert_name, alert_name, int(severity), |
| ToEpoch(now), ToEpoch(build['finish_time'] or now), |
| links, [], 'cros-failure', |
| som.CrosBuildFailure(notes, alert_stages, builders)) |
| |
| |
| def GenerateAlertsSummary(db, builds=None, |
| logdog_client=None, milo_client=None, |
| allow_experimental=False): |
| """Generates the full set of alerts to send to Sheriff-o-Matic. |
| |
| Args: |
| db: cidb.CIDBConnection object. |
| builds: A list of (waterfall, builder_name, severity) tuples to summarize. |
| Defaults to SOM_BUILDS[SOM_TREE]. |
| logdog_client: logdog.LogdogClient object. |
| milo_client: milo.MiloClient object. |
| allow_experimental: Boolean if non-important builds should be included. |
| |
| Returns: |
| JSON-marshalled AlertsSummary object. |
| """ |
| if not builds: |
| builds = constants.SOM_BUILDS[constants.SOM_TREE] |
| if not logdog_client: |
| logdog_client = logdog.LogdogClient() |
| if not milo_client: |
| milo_client = milo.MiloClient() |
| |
| funcs = [] |
| now = datetime.datetime.utcnow() |
| |
| # Iterate over relevant masters. |
| # build_tuple is either: waterfall, build_config, severity |
| # or: build_id, severity |
| for build_tuple in builds: |
| # Find the specified build. |
| if len(build_tuple) == 2: |
| # pylint: disable=unbalanced-tuple-unpacking |
| build_id, severity = build_tuple |
| # pylint: enable=unbalanced-tuple-unpacking |
| master = db.GetBuildStatus(build_id) |
| if master is None: |
| logging.warn('Could not locate build id %s', build_id) |
| continue |
| wfall = master['waterfall'] |
| build_config = master['build_config'] |
| elif len(build_tuple) == 3: |
| wfall, build_config, severity = build_tuple |
| master = db.GetMostRecentBuild(wfall, build_config) |
| if master is None: |
| logging.warn('Could not locate build %s %s', wfall, build_config) |
| continue |
| else: |
| logging.error('Invalid build tuple: %s' % str(build_tuple)) |
| continue |
| |
| statuses = [master] |
| stages = db.GetBuildStages(master['id']) |
| exceptions = db.GetBuildsFailures([master['id']]) |
| messages = db.GetBuildMessages(master['id']) |
| annotations = [] |
| logging.info('%s %s (id %d): single/master build, %d stages, %d messages', |
| wfall, build_config, master['id'], |
| len(stages), len(messages)) |
| |
| # Find any slave builds, and the individual slave stages. |
| slave_statuses = db.GetSlaveStatuses(master['id']) |
| if len(slave_statuses): |
| statuses.extend(slave_statuses) |
| slave_stages = db.GetSlaveStages(master['id']) |
| stages.extend(slave_stages) |
| exceptions.extend(db.GetSlaveFailures(master['id'])) |
| annotations.extend(db.GetAnnotationsForBuilds( |
| [master['id']]).get(master['id'], [])) |
| logging.info('- %d slaves, %d slave stages, %d annotations', |
| len(slave_statuses), len(slave_stages), len(annotations)) |
| |
| # Look for failing and inflight (signifying timeouts) slave builds. |
| for build in sorted(statuses, key=lambda s: s['builder_name']): |
| funcs.append(lambda build_=build, stages_=stages, exceptions_=exceptions, |
| messages_=messages, annotations_=annotations, |
| siblings_=statuses, severity_=severity: |
| GenerateBuildAlert(build_, stages_, exceptions_, messages_, |
| annotations_, siblings_, severity_, |
| now, db, logdog_client, milo_client, |
| allow_experimental=allow_experimental)) |
| |
| alerts = [alert for alert in parallel.RunParallelSteps(funcs, |
| return_values=True) |
| if alert] |
| |
| revision_summaries = {} |
| summary = som.AlertsSummary(alerts, revision_summaries, ToEpoch(now)) |
| |
| return json.dumps(summary, cls=ObjectEncoder) |
| |
| |
| def ToEpoch(value): |
| """Convert a datetime to number of seconds past epoch.""" |
| epoch = datetime.datetime(1970, 1, 1) |
| return (value - epoch).total_seconds() |
| |
| |
| def main(argv): |
| parser = GetParser() |
| options = parser.parse_args(argv) |
| |
| # Determine which hosts to connect to. |
| db = cidb.CIDBConnection(options.cred_dir) |
| topology.FetchTopologyFromCIDB(db) |
| |
| if options.json_file: |
| # Use the specified alerts. |
| logging.info('Using JSON file %s', options.json_file) |
| with open(options.json_file) as f: |
| summary_json = f.read() |
| print(summary_json) |
| else: |
| builds = [tuple(x.split(',')) for x in options.builds] |
| if not builds: |
| builds = constants.SOM_BUILDS[options.som_tree] |
| |
| # Generate the set of alerts to send. |
| logdog_client = logdog.LogdogClient(options.service_acct_json, |
| host=options.logdog_host) |
| milo_client = milo.MiloClient(options.service_acct_json, |
| host=options.milo_host) |
| summary_json = GenerateAlertsSummary( |
| db, builds=builds, logdog_client=logdog_client, milo_client=milo_client, |
| allow_experimental=options.allow_experimental) |
| if options.output_json: |
| with open(options.output_json, 'w') as f: |
| logging.info('Writing JSON file %s', options.output_json) |
| f.write(summary_json) |
| |
| # Authenticate and send the alerts. |
| som_client = som.SheriffOMaticClient(options.service_acct_json, |
| insecure=options.som_insecure, |
| host=options.som_host) |
| som_client.SendAlerts(summary_json, tree=options.som_tree) |