def _send_mail(from_addr, to_addr, body): """ Send emails with smtplib. This is a lower level function than send_e-mail(). Args: from_addr (str): The e-mail address to use in the envelope from field. to_addr (str): The e-mail address to use in the envelope to field. body (str): The body of the e-mail. """ smtp_server = config.get('smtp_server') if not smtp_server: log.info('Not sending email: No smtp_server defined') return smtp = None try: log.debug('Connecting to %s', smtp_server) smtp = smtplib.SMTP(smtp_server) smtp.sendmail(from_addr, [to_addr], body) except smtplib.SMTPRecipientsRefused as e: log.warning('"recipient refused" for %r, %r' % (to_addr, e)) except Exception: log.exception('Unable to send mail') finally: if smtp: smtp.quit()
def exception_json_view(exc, request): """ Return a json error response upon generic errors (404s, 403s, 500s, etc..). This is here to catch everything that isn't caught by our cornice error handlers. When we do catch something, we transform it into a cornice Errors object and pass it to our nice cornice error handler. That way, all the exception presentation and rendering we can keep in one place. Args: exc (Exception): The unhandled exception. request (pyramid.request.Request): The current request. Returns: bodhi.server.services.errors.json_handler: A pyramid.httpexceptions.HTTPError to be rendered to the user for the given exception. """ errors = getattr(request, 'errors', []) status = getattr(exc, 'status_code', 500) if status not in (404, 403): log.exception("Error caught. Handling JSON response.") else: log.warning(str(exc)) if not len(errors): description = getattr(exc, 'explanation', None) or str(exc) errors = cornice.errors.Errors(status=status) errors.add('body', description=description, name=exc.__class__.__name__) request.errors = errors return bodhi.server.services.errors.json_handler(request)
def new_comment(request): """ Add a new comment to an update. Args: request (pyramid.request): The current request. Returns: dict: A dictionary with two keys. "comment" indexes the new comment, and "caveats" indexes an iterable of messages to display to the user. """ data = request.validated # This has already been validated at this point, but we need to ditch # it since the models don't care about a csrf argument. data.pop('csrf_token') update = data.pop('update') author = request.user and request.user.name try: comment, caveats = update.comment(session=request.db, author=author, **data) except ValueError as e: request.errors.add('body', 'comment', str(e)) return except Exception as e: log.exception(e) request.errors.add('body', 'comment', 'Unable to create comment') return return dict(comment=comment, caveats=caveats)
def new_comment(request): """ Add a new comment to an update. Args: request (pyramid.request): The current request. Returns: dict: A dictionary with two keys. "comment" indexes the new comment, and "caveats" indexes an iterable of messages to display to the user. """ data = request.validated # This has already been validated at this point, but we need to ditch # it since the models don't care about a csrf argument. data.pop('csrf_token') update = data.pop('update') email = data.pop('email', None) author = email or (request.user and request.user.name) anonymous = bool(email) or not author if not author: request.errors.add('body', 'email', 'You must provide an author') request.errors.status = HTTPBadRequest.code return try: comment, caveats = update.comment( session=request.db, author=author, anonymous=anonymous, **data) except Exception as e: log.exception(e) request.errors.add('body', 'comment', 'Unable to create comment') return return dict(comment=comment, caveats=caveats)
def waive_test_results(request): """ Waive all blocking test results on a given update when gating is on. Args: request (pyramid.request): The current request. Returns: dict: A dictionary mapping the key "update" to the update. """ update = request.validated['update'] comment = request.validated.pop('comment', None) tests = request.validated.pop('tests', None) try: update.waive_test_results(request.user.name, comment, tests) except LockedUpdateException as e: log.warning(str(e)) request.errors.add('body', 'request', str(e)) except BodhiException as e: log.error("Failed to waive the test results: %s", e) request.errors.add('body', 'request', str(e)) except Exception as e: log.exception("Unhandled exception in waive_test_results") request.errors.add('body', 'request', str(e)) return dict(update=update)
def __call__(self): """ Manage a database Session object for the life of the context. Yields a database Session object, then either commits the tranaction if there were no Exceptions or rolls back the transaction. In either case, it also will close and remove the Session. """ session = Session() try: yield session session.commit() except Exception as e: # It is possible for session.rolback() to raise Exceptions, so we will wrap it in an # Exception handler as well so we can log the rollback failure and still raise the # original Exception. try: session.rollback() except Exception: log.exception( 'An Exception was raised while rolling back a transaction.' ) raise e finally: session.close() Session.remove()
def get_test_results(request): """ Get the test results on a given update when gating is on. Args: request (pyramid.request): The current request. Returns: dict: A dictionary mapping the key 'decisions' to a list of result dictionaries. """ update = request.validated['update'] decisions = [] try: decisions = update.get_test_gating_info() except RequestsTimeout as e: log.error("Error querying greenwave for test results - timed out") request.errors.add('body', 'request', str(e)) request.errors.status = 504 except (RequestException, RuntimeError) as e: log.error("Error querying greenwave for test results: %s", e) request.errors.add('body', 'request', str(e)) request.errors.status = 502 except BodhiException as e: log.error("Failed to query greenwave for test results: %s", e) request.errors.add('body', 'request', str(e)) request.errors.status = 501 except Exception as e: log.exception("Unhandled exception in get_test_results") request.errors.add('body', 'request', str(e)) request.errors.status = 500 return dict(decisions=decisions)
def save_release(request): """ Save a release. This entails either creating a new release, or editing an existing one. To edit an existing release, the release's original name must be specified in the ``edited`` parameter. Args: request (pyramid.request): The current request. Returns: bodhi.server.models.Request: The created or edited Request. """ data = request.validated edited = data.pop("edited", None) # This has already been validated at this point, but we need to ditch # it since the models don't care about a csrf argument. data.pop('csrf_token', None) try: if edited is None: log.info("Creating a new release: %s" % data['name']) r = Release(**data) else: log.info("Editing release: %s" % edited) r = request.db.query(Release).filter(Release.name == edited).one() for k, v in data.items(): # We have to change updates status to obsolete # if state of release changes to archived if k == "state" and v == ReleaseState.archived and \ r.state != ReleaseState.archived: updates = request.db.query(Update).filter(Update.release_id == r.id).filter( Update.status.notin_( [UpdateStatus.obsolete, UpdateStatus.stable, UpdateStatus.unpushed] ) ).all() for u in updates: u.status = UpdateStatus.obsolete u.comment( request.db, 'This update is marked obsolete because ' 'the {} release is archived.'.format(u.release.name), author='bodhi', ) setattr(r, k, v) except Exception as e: log.exception(e) request.errors.add('body', 'release', 'Unable to create/edit release: %s' % e) return request.db.add(r) request.db.flush() return r
def expire_buildroot_overrides(self): """ Expire any buildroot overrides that are in this push """ for update in self.updates: if update.request is UpdateRequest.stable: for build in update.builds: if build.override: try: build.override.expire() except Exception: log.exception('Problem expiring override')
def set_request(request): """ Set a specific :class:`bodhi.server.models.UpdateRequest` on a given update. Args: request (pyramid.request): The current request. Returns: dict: A dictionary mapping the key "update" to the update that was modified. """ update = request.validated['update'] action = request.validated['request'] if update.locked: request.errors.add('body', 'request', "Can't change request on a locked update") return if update.release.state is ReleaseState.archived: request.errors.add('body', 'request', "Can't change request for an archived release") return if update.status == UpdateStatus.stable and action == UpdateRequest.testing: request.errors.add( 'body', 'request', "Pushing back to testing a stable update is not allowed") return if action == UpdateRequest.stable: settings = request.registry.settings result, reason = update.check_requirements(request.db, settings) log.info( f'Unable to set request for {update.alias} to {action} due to failed requirements: ' f'{reason}') if not result: request.errors.add('body', 'request', 'Requirement not met %s' % reason) return try: update.set_request(request.db, action, request.user.name) except BodhiException as e: log.info("Failed to set the request: %s", e) request.errors.add('body', 'request', str(e)) except Exception as e: log.exception("Unhandled exception in set_request") request.errors.add('body', 'request', str(e)) return dict(update=update)
def taskotron_results(settings, entity='results/latest', max_queries=10, **kwargs): """ Yield resultsdb results using query arguments. Args: settings (bodhi.server.config.BodhiConfig): Bodhi's settings. entity (str): The API endpoint to use (see resultsdb documentation). max_queries (int): The maximum number of queries to perform (pages to retrieve). ``1`` means just a single page. ``None`` or ``0`` means no limit. Please note some tests might have thousands of results in the database and it's very reasonable to limit queries (thus the default value). kwargs (dict): Args that will be passed to resultsdb to specify what results to retrieve. Returns: generator or None: Yields Python objects loaded from ResultsDB's "data" field in its JSON response, or None if there was an Exception while performing the query. """ max_queries = max_queries or 0 url = settings['resultsdb_api_url'] + "/api/v2.0/" + entity if kwargs: url = url + "?" + urlencode(kwargs) data = True queries = 0 try: while data and url: log.debug("Grabbing %r" % url) response = requests.get(url, timeout=60) if response.status_code != 200: raise IOError("status code was %r" % response.status_code) json = response.json() for datum in json['data']: yield datum url = json.get('next') queries += 1 if max_queries and queries >= max_queries and url: log.debug('Too many result pages, aborting at: %r' % url) break except Exception as e: log.exception("Problem talking to %r : %r" % (url, str(e)))
def save_release(request): """ Save a release. This entails either creating a new release, or editing an existing one. To edit an existing release, the release's original name must be specified in the ``edited`` parameter. Args: request (pyramid.request): The current request. Returns: bodhi.server.models.Request: The created or edited Request. """ data = request.validated edited = data.pop("edited", None) # This has already been validated at this point, but we need to ditch # it since the models don't care about a csrf argument. data.pop('csrf_token', None) try: if edited is None: log.info("Creating a new release: %s" % data['name']) r = Release(**data) else: log.info("Editing release: %s" % edited) r = request.db.query(Release).filter(Release.name == edited).one() for k, v in data.items(): setattr(r, k, v) except Exception as e: log.exception(e) request.errors.add('body', 'release', 'Unable to create update: %s' % e) return request.db.add(r) request.db.flush() return r
def set_request(request): """ Set a specific :class:`bodhi.server.models.UpdateRequest` on a given update. Args: request (pyramid.request): The current request. Returns: dict: A dictionary mapping the key "update" to the update that was modified. """ update = request.validated['update'] action = request.validated['request'] if update.locked: request.errors.add('body', 'request', "Can't change request on a locked update") return if update.release.state is ReleaseState.archived: request.errors.add('body', 'request', "Can't change request for an archived release") return if action in (UpdateRequest.stable, UpdateRequest.batched): settings = request.registry.settings result, reason = update.check_requirements(request.db, settings) if not result: request.errors.add('body', 'request', 'Requirement not met %s' % reason) return try: update.set_request(request.db, action, request.user.name) except BodhiException as e: log.exception("Failed to set the request") request.errors.add('body', 'request', str(e)) except Exception as e: log.exception("Unhandled exception in set_request") request.errors.add('body', 'request', str(e)) return dict(update=update)
def new_comment(request): """ Add a new comment to an update. Args: request (pyramid.request): The current request. Returns: dict: A dictionary with two keys. "comment" indexes the new comment, and "caveats" indexes an iterable of messages to display to the user. """ data = request.validated # This has already been validated at this point, but we need to ditch # it since the models don't care about a csrf argument. data.pop('csrf_token') update = data.pop('update') author = request.user and request.user.name if not author: # this can happen if we have a stale cached session, a 403 # response will trigger the client to reauth: # https://github.com/fedora-infra/bodhi/issues/3298 request.errors.add('body', 'email', 'You must provide an author') request.errors.status = HTTPForbidden.code return try: comment, caveats = update.comment(session=request.db, author=author, **data) except ValueError as e: request.errors.add('body', 'comment', str(e)) return except Exception as e: log.exception(e) request.errors.add('body', 'comment', 'Unable to create comment') return return dict(comment=comment, caveats=caveats)
def save_override(request): """ Create or edit a buildroot override. This entails either creating a new buildroot override, or editing an existing one. To edit an existing buildroot override, the buildroot override's original id needs to be specified in the ``edited`` parameter. Args: request (pyramid.request): The current web request. Returns: dict: The new or edited override. """ data = request.validated edited = data.pop("edited") caveats = [] try: submitter = User.get(request.user.name) if edited is None: builds = data['builds'] overrides = [] if len(builds) > 1: caveats.append({ 'name': 'nvrs', 'description': 'Your override submission was ' 'split into %i.' % len(builds) }) for build in builds: log.info("Creating a new buildroot override: %s" % build.nvr) if BuildrootOverride.get(build.id): request.errors.add('body', 'builds', 'Buildroot override for %s already exists' % build.nvr) return else: overrides.append(BuildrootOverride.new( request, build=build, submitter=submitter, notes=data['notes'], expiration_date=data['expiration_date'], )) if len(builds) > 1: result = dict(overrides=overrides) else: result = overrides[0] else: log.info("Editing buildroot override: %s" % edited) edited = Build.get(edited) if edited is None: request.errors.add('body', 'edited', 'No such build') return result = BuildrootOverride.edit( request, edited=edited, submitter=submitter, notes=data["notes"], expired=data["expired"], expiration_date=data["expiration_date"]) if not result: # Some error inside .edit(...) return except Exception as e: log.exception(e) request.errors.add('body', 'override', 'Unable to save buildroot override: %s' % e) return if not isinstance(result, dict): result = result.__json__() result['caveats'] = caveats return result
def new_update(request): """ Save an update. This entails either creating a new update, or editing an existing one. To edit an existing update, the update's alias must be specified in the ``edited`` parameter. If the ``from_tag`` parameter is specified and ``builds`` is missing or empty, the list of builds will be filled with the latest builds in this Koji tag. This is done by validate_from_tag() because the list of builds needs to be available in validate_acls(). If the release is composed by Bodhi (i.e. a branched or stable release after the Bodhi activation point), ensure that related tags ``from_tag``-pending-signing and ``from_tag``-testing exists and if not create them in Koji. If the state of the release is not `pending`, add its pending-signing tag and remove it if it's a side tag. Args: request (pyramid.request): The current request. """ data = request.validated log.debug('validated = %s' % data) # This has already been validated at this point, but we need to ditch # it since the models don't care about a csrf argument. data.pop('csrf_token') # Same here, but it can be missing. data.pop('builds_from_tag', None) data.pop('sidetag_owner', None) build_nvrs = data.get('builds', []) from_tag = data.get('from_tag') caveats = [] try: releases = set() builds = [] # Create the Package and Build entities for nvr in build_nvrs: name, version, release = request.buildinfo[nvr]['nvr'] package = Package.get_or_create(request.db, request.buildinfo[nvr]) # Also figure out the build type and create the build if absent. build_class = ContentType.infer_content_class( base=Build, build=request.buildinfo[nvr]['info']) build = build_class.get(nvr) if build is None: log.debug("Adding nvr %s, type %r", nvr, build_class) build = build_class(nvr=nvr, package=package) request.db.add(build) request.db.flush() build.package = package build.release = request.buildinfo[build.nvr]['release'] builds.append(build) releases.add(request.buildinfo[build.nvr]['release']) # Disable manual updates for releases not composed by Bodhi # see #4058 if not from_tag: for release in releases: if not release.composed_by_bodhi: request.errors.add( 'body', 'builds', "Cannot manually create updates for a Release which is not " "composed by Bodhi.\nRead the 'Automatic updates' page in " "Bodhi docs about this error.") request.db.rollback() return # We want to go ahead and commit the transaction now so that the Builds are in the database. # Otherwise, there will be a race condition between robosignatory signing the Builds and the # signed handler attempting to mark the builds as signed. When we lose that race, the signed # handler doesn't see the Builds in the database and gives up. After that, nothing will mark # the builds as signed. request.db.commit() # After we commit the transaction, we need to get the builds and releases again, # since they were tied to the previous session that has now been terminated. builds = [] releases = set() for nvr in build_nvrs: # At this moment, we are sure the builds are in the database (that is what the commit # was for actually). build = Build.get(nvr) builds.append(build) releases.add(build.release) if data.get('edited'): log.info('Editing update: %s' % data['edited']) data['release'] = list(releases)[0] data['builds'] = [b.nvr for b in builds] data['from_tag'] = from_tag result, _caveats = Update.edit(request, data) caveats.extend(_caveats) else: if len(releases) > 1: caveats.append({ 'name': 'releases', 'description': 'Your update is being split ' 'into %i, one for each release.' % len(releases) }) updates = [] for release in releases: _data = copy.copy(data) # Copy it because .new(..) mutates it _data['builds'] = [b for b in builds if b.release == release] _data['release'] = release _data['from_tag'] = from_tag log.info('Creating new update: %r' % _data['builds']) result, _caveats = Update.new(request, _data) log.debug('%s update created', result.alias) updates.append(result) caveats.extend(_caveats) if len(releases) > 1: result = dict(updates=updates) if from_tag: for u in updates: builds = [b.nvr for b in u.builds] if not u.release.composed_by_bodhi: # Before the Bodhi activation point of a release, keep builds tagged # with the side-tag and its associate tags. side_tag_signing_pending = u.release.get_pending_signing_side_tag( from_tag) side_tag_testing_pending = u.release.get_pending_testing_side_tag( from_tag) handle_side_and_related_tags_task.delay( builds=builds, pending_signing_tag=side_tag_signing_pending, from_tag=from_tag, pending_testing_tag=side_tag_testing_pending) else: # After the Bodhi activation point of a release, add the pending-signing tag # of the release to funnel the builds back into a normal workflow for a # stable release. pending_signing_tag = u.release.pending_signing_tag candidate_tag = u.release.candidate_tag handle_side_and_related_tags_task.delay( builds=builds, pending_signing_tag=pending_signing_tag, from_tag=from_tag, candidate_tag=candidate_tag) except LockedUpdateException as e: log.warning(str(e)) request.errors.add('body', 'builds', "%s" % str(e)) return except Exception as e: log.exception('Failed to create update') request.errors.add('body', 'builds', 'Unable to create update. %s' % str(e)) return # Obsolete older updates for three different cases... # editing an update, submitting a new single update, submitting multiple. if isinstance(result, dict): updates = result['updates'] else: updates = [result] for update in updates: try: caveats.extend(update.obsolete_older_updates(request.db)) except Exception as e: caveats.append({ 'name': 'update', 'description': 'Problem obsoleting older updates: %s' % str(e), }) if not isinstance(result, dict): result = result.__json__() result['caveats'] = caveats return result
def save_override(request): """ Create or edit a buildroot override. This entails either creating a new buildroot override, or editing an existing one. To edit an existing buildroot override, the buildroot override's original id needs to be specified in the ``edited`` parameter. Args: request (pyramid.request): The current web request. Returns: dict: The new or edited override. """ data = request.validated edited = data.pop("edited") caveats = [] try: submitter = User.get(request.user.name) if edited is None: builds = data['builds'] overrides = [] if len(builds) > 1: caveats.append({ 'name': 'nvrs', 'description': 'Your override submission was ' 'split into %i.' % len(builds) }) for build in builds: log.info("Creating a new buildroot override: %s" % build.nvr) existing_override = BuildrootOverride.get(build.id) if existing_override: if not existing_override.expired_date: data['expiration_date'] = max( existing_override.expiration_date, data['expiration_date']) new_notes = f"""{data['notes']} _____________ _@{existing_override.submitter.name} ({existing_override.submission_date.strftime('%b %d, %Y')})_ {existing_override.notes}""" # Truncate notes at 2000 chars if len(new_notes) > 2000: new_notes = new_notes[:1972] + '(...)\n___Notes truncated___' overrides.append( BuildrootOverride.edit( request, edited=build, submitter=submitter, submission_date=datetime.now(), notes=new_notes, expiration_date=data['expiration_date'], expired=None, )) else: overrides.append( BuildrootOverride.new( request, build=build, submitter=submitter, notes=data['notes'], expiration_date=data['expiration_date'], )) if len(builds) > 1: result = dict(overrides=overrides) else: result = overrides[0] else: log.info("Editing buildroot override: %s" % edited) edited = Build.get(edited) if edited is None: request.errors.add('body', 'edited', 'No such build') return result = BuildrootOverride.edit( request, edited=edited, submitter=submitter, notes=data["notes"], expired=data["expired"], expiration_date=data["expiration_date"]) if not result: # Some error inside .edit(...) return except Exception as e: log.exception(e) request.errors.add('body', 'override', 'Unable to save buildroot override: %s' % e) return if not isinstance(result, dict): result = result.__json__() result['caveats'] = caveats return result
def new_update(request): """ Save an update. This entails either creating a new update, or editing an existing one. To edit an existing update, the update's alias must be specified in the ``edited`` parameter. Args: request (pyramid.request): The current request. """ data = request.validated log.debug('validated = %s' % data) # This has already been validated at this point, but we need to ditch # it since the models don't care about a csrf argument. data.pop('csrf_token') caveats = [] try: releases = set() builds = [] # Create the Package and Build entities for nvr in data['builds']: name, version, release = request.buildinfo[nvr]['nvr'] package = Package.get_or_create(request.buildinfo[nvr]) # Also figure out the build type and create the build if absent. build_class = ContentType.infer_content_class( base=Build, build=request.buildinfo[nvr]['info']) build = build_class.get(nvr) if build is None: log.debug("Adding nvr %s, type %r", nvr, build_class) build = build_class(nvr=nvr, package=package) request.db.add(build) request.db.flush() build.package = package build.release = request.buildinfo[build.nvr]['release'] builds.append(build) releases.add(request.buildinfo[build.nvr]['release']) # We want to go ahead and commit the transaction now so that the Builds are in the database. # Otherwise, there will be a race condition between robosignatory signing the Builds and the # signed handler attempting to mark the builds as signed. When we lose that race, the signed # handler doesn't see the Builds in the database and gives up. After that, nothing will mark # the builds as signed. request.db.commit() # After we commit the transaction, we need to get the builds and releases again, since they # were tied to the previous session that has now been terminated. builds = [] releases = set() for nvr in data['builds']: # At this moment, we are sure the builds are in the database (that is what the commit # was for actually). build = Build.get(nvr) builds.append(build) releases.add(build.release) if data.get('edited'): log.info('Editing update: %s' % data['edited']) data['release'] = list(releases)[0] data['builds'] = [b.nvr for b in builds] result, _caveats = Update.edit(request, data) caveats.extend(_caveats) else: if len(releases) > 1: caveats.append({ 'name': 'releases', 'description': 'Your update is being split ' 'into %i, one for each release.' % len(releases) }) updates = [] for release in releases: _data = copy.copy(data) # Copy it because .new(..) mutates it _data['builds'] = [b for b in builds if b.release == release] _data['release'] = release log.info('Creating new update: %r' % _data['builds']) result, _caveats = Update.new(request, _data) log.debug('%s update created', result.alias) updates.append(result) caveats.extend(_caveats) if len(releases) > 1: result = dict(updates=updates) except LockedUpdateException as e: log.warning(str(e)) request.errors.add('body', 'builds', "%s" % str(e)) return except Exception as e: log.exception('Failed to create update') request.errors.add('body', 'builds', 'Unable to create update. %s' % str(e)) return # Obsolete older updates for three different cases... # editing an update, submitting a new single update, submitting multiple. if isinstance(result, dict): updates = result['updates'] else: updates = [result] for update in updates: try: caveats.extend(update.obsolete_older_updates(request.db)) except Exception as e: caveats.append({ 'name': 'update', 'description': 'Problem obsoleting older updates: %s' % str(e), }) if not isinstance(result, dict): result = result.__json__() result['caveats'] = caveats return result