def _finish_login_pipeline(self, identity): """ The login flow executes both with anonymous and authenticated users. Upon completion a few branches exist: If the identity is already linked, the user should be logged in and redirected immediately. Otherwise, the user is presented with a confirmation window. That window will show them the new account that will be created, and if they're already authenticated an optional button to associate the identity with their account. """ auth_provider = self.auth_provider lock = locks.get( 'sso:auth:{}:{}'.format( auth_provider.id, md5(unicode(identity['id'])).hexdigest(), ), duration=5, ) with TimedRetryPolicy(5)(lock.acquire): try: auth_identity = AuthIdentity.objects.get( auth_provider=auth_provider, ident=identity['id'], ) except AuthIdentity.DoesNotExist: return self._handle_unknown_identity(identity) return self._handle_existing_identity(auth_identity, identity)
def _finish_login_pipeline(self, identity): """ The login flow executes both with anonymous and authenticated users. Upon completion a few branches exist: If the identity is already linked, the user should be logged in and redirected immediately. Otherwise, the user is presented with a confirmation window. That window will show them the new account that will be created, and if they're already authenticated an optional button to associate the identity with their account. """ auth_provider = self.auth_provider user_id = identity['id'] lock = locks.get( 'sso:auth:{}:{}'.format( auth_provider.id, md5_text(user_id).hexdigest(), ), duration=5, ) with TimedRetryPolicy(5)(lock.acquire): try: auth_identity = AuthIdentity.objects.select_related('user').get( auth_provider=auth_provider, ident=user_id, ) except AuthIdentity.DoesNotExist: auth_identity = None # Handle migration of identity keys if not auth_identity and isinstance(user_id, MigratingIdentityId): try: auth_identity = AuthIdentity.objects.select_related('user').get( auth_provider=auth_provider, ident=user_id.legacy_id, ) auth_identity.update(ident=user_id.id) except AuthIdentity.DoesNotExist: auth_identity = None if not auth_identity: return self._handle_unknown_identity(identity) # If the User attached to this AuthIdentity is not active, # we want to clobber the old account and take it over, rather than # getting logged into the inactive account. if not auth_identity.user.is_active: # Current user is also not logged in, so we have to # assume unknown. if not self.request.user.is_authenticated(): return self._handle_unknown_identity(identity) auth_identity = self._handle_attach_identity(identity) return self._handle_existing_identity(auth_identity, identity)
def test_commits_lock_conflict(self): user = self.create_user(is_staff=False, is_superuser=False) org = self.create_organization() org.flags.allow_joinleave = False org.save() team = self.create_team(organization=org) project = self.create_project(name="foo", organization=org, teams=[team]) self.create_member(teams=[team], user=user, organization=org) self.login_as(user=user) release = self.create_release(project, self.user, version="1.2.1") release.add_project(project) # Simulate a concurrent request by using an existing release # that has its commit lock taken out. lock = locks.get(Release.get_lock_key(org.id, release.id), duration=10) lock.acquire() url = reverse( "sentry-api-0-organization-release-details", kwargs={"organization_slug": org.slug, "version": release.version}, ) response = self.client.put(url, data={"commits": [{"id": "a" * 40}, {"id": "b" * 40}]}) assert response.status_code == 409, (response.status_code, response.content) assert "Release commits" in response.data["detail"]
def post(self, request, organization): """ Add a invite request to Organization ```````````````````````````````````` Creates an invite request given an email and sugested role / teams. :pparam string organization_slug: the slug of the organization the member will belong to :param string email: the email address to invite :param string role: the suggested role of the new member :param array teams: the suggested slugs of the teams the member should belong to. :auth: required """ if not features.has("organizations:invite-members", organization, actor=request.user): return Response( { "organization": "Your organization is not allowed to invite members" }, status=403) serializer = OrganizationMemberSerializer( data=request.data, context={ "organization": organization, "allowed_roles": roles.get_all() }, ) if not serializer.is_valid(): return Response(serializer.errors, status=400) result = serializer.validated_data with transaction.atomic(): om = OrganizationMember.objects.create( organization=organization, email=result["email"], role=result["role"], inviter=request.user, invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value, ) if result["teams"]: lock = locks.get(u"org:member:{}".format(om.id), duration=5) with TimedRetryPolicy(10)(lock.acquire): save_team_assignments(om, result["teams"]) self.create_audit_entry( request=request, organization_id=organization.id, target_object=om.id, data=om.get_audit_log_data(), event=AuditLogEntryEvent.INVITE_REQUEST_ADD, ) return Response(serialize(om), status=201)
def _ensure_blob(self, orm, file): from sentry.app import locks from sentry.utils.retries import TimedRetryPolicy File = orm['sentry.File'] FileBlob = orm['sentry.FileBlob'] lock = locks.get('fileblob:convert:{}'.format(file.checksum), duration=60) with TimedRetryPolicy(60 * 5)(lock.acquire): if not file.storage: return try: blob = FileBlob.objects.get(checksum=file.checksum) except FileBlob.DoesNotExist: blob = FileBlob.objects.create( checksum=file.checksum, storage=file.storage, storage_options=file.storage_options, path=file.path, size=file.size, timestamp=file.timestamp, ) File.objects.filter( id=file.id, ).update(blob=blob) file.blob = blob
def start_release(self, version, **values): values.setdefault('date_started', timezone.now()) affected = Release.objects.filter( version=version, organization_id=self.project.organization_id, projects=self.project, ).update(**values) if not affected: release = Release.objects.filter( version=version, organization_id=self.project.organization_id, ).first() if release: release.update(**values) else: lock_key = Release.get_lock_key(self.project.organization_id, version) lock = locks.get(lock_key, duration=5) with TimedRetryPolicy(10)(lock.acquire): try: release = Release.objects.get( version=version, organization_id=self.project.organization_id) except Release.DoesNotExist: release = Release.objects.create( version=version, organization_id=self.project.organization_id, **values) release.add_project(self.project)
def test_release_locked(self): self.login_as(user=self.user) org = self.create_organization(owner=self.user, name="baz") repo = Repository.objects.create(name="example", provider="dummy", organization_id=org.id) old_release = Release.objects.create(organization_id=org.id, version="abcabcabc") commit = Commit.objects.create(organization_id=org.id, repository_id=repo.id, key="a" * 40) ReleaseHeadCommit.objects.create(organization_id=org.id, repository_id=repo.id, release=old_release, commit=commit) refs = [{"repository": repo.name, "commit": "b" * 40}] new_release = Release.objects.create(organization_id=org.id, version="12345678") lock = locks.get(Release.get_lock_key(org.id, new_release.id), duration=10) lock.acquire() with self.tasks(): fetch_commits( release_id=new_release.id, user_id=self.user.id, refs=refs, previous_release_id=old_release.id, ) count_query = ReleaseHeadCommit.objects.filter(release=new_release) # No release commits should be made as the task should return early. assert count_query.count() == 0
def start_release(self, version, **values): values.setdefault('date_started', timezone.now()) affected = Release.objects.filter( version=version, organization_id=self.project.organization_id, projects=self.project, ).update(**values) if not affected: release = Release.objects.filter( version=version, organization_id=self.project.organization_id, ).first() if release: release.update(**values) else: lock_key = Release.get_lock_key(self.project.organization_id, version) lock = locks.get(lock_key, duration=5) with TimedRetryPolicy(10)(lock.acquire): try: release = Release.objects.get( version=version, organization_id=self.project.organization_id ) except Release.DoesNotExist: release = Release.objects.create( version=version, organization_id=self.project.organization_id, **values ) release.add_project(self.project)
def delete(self, *args, **kwargs): lock = locks.get('fileblob:upload:{}'.format(self.checksum), duration=60 * 10) with TimedRetryPolicy(60)(lock.acquire): if self.path: self.deletefile(commit=False) super(FileBlob, self).delete(*args, **kwargs)
def delete(self, *args, **kwargs): lock = locks.get("fileblob:upload:{}".format(self.checksum), duration=UPLOAD_RETRY_TIME) with TimedRetryPolicy(UPLOAD_RETRY_TIME, metric_instance="lock.fileblob.delete")( lock.acquire ): super().delete(*args, **kwargs) if self.path: self.deletefile(commit=False)
def save(self, *args, **kwargs): if not self.slug: lock = locks.get('slug:project', duration=5) with TimedRetryPolicy(10)(lock.acquire): slugify_instance(self, self.name, organization=self.organization) super(Project, self).save(*args, **kwargs) else: super(Project, self).save(*args, **kwargs)
def save(self, *args, **kwargs): if not self.slug: lock = locks.get("slug:team", duration=5) with TimedRetryPolicy(10)(lock.acquire): slugify_instance(self, self.name, organization=self.organization) super().save(*args, **kwargs)
def get_security_token(self): lock = locks.get(self.get_lock_key(), duration=5) with TimedRetryPolicy(10)(lock.acquire): security_token = self.get_option('sentry:token', None) if security_token is None: security_token = uuid1().hex self.update_option('sentry:token', security_token) return security_token
def save(self, *args, **kwargs): if not self.slug: lock = locks.get('slug:organization', duration=5) with TimedRetryPolicy(10)(lock.acquire): slugify_instance(self, self.name, reserved=RESERVED_ORGANIZATION_SLUGS) super(Organization, self).save(*args, **kwargs) else: super(Organization, self).save(*args, **kwargs)
def delete_file(path, checksum, **kwargs): from sentry.models.file import get_storage, FileBlob from sentry.app import locks from sentry.utils.retries import TimedRetryPolicy lock = locks.get(u'fileblob:upload:{}'.format(checksum), duration=60 * 10) with TimedRetryPolicy(60)(lock.acquire): if not FileBlob.objects.filter(checksum=checksum).exists(): get_storage().delete(path)
def delete_file(path, checksum, **kwargs): from sentry.models.file import get_storage, FileBlob from sentry.app import locks from sentry.utils.retries import TimedRetryPolicy lock = locks.get(f"fileblob:upload:{checksum}", duration=60 * 10) with TimedRetryPolicy(60)(lock.acquire): if not FileBlob.objects.filter(checksum=checksum).exists(): get_storage().delete(path)
def _locked_blob(checksum): lock = locks.get(u'fileblob:upload:{}'.format(checksum), duration=UPLOAD_RETRY_TIME) with TimedRetryPolicy(UPLOAD_RETRY_TIME)(lock.acquire): # test for presence try: existing = FileBlob.objects.get(checksum=checksum) except FileBlob.DoesNotExist: existing = None yield existing
def process_suspect_commits(event_id, event_platform, event_frames, group_id, project_id, **kwargs): lock = locks.get(f"process-suspect-commits:{group_id}", duration=10) try: with lock.acquire(): _process_suspect_commits(event_id, event_platform, event_frames, group_id, project_id, **kwargs) except UnableToAcquireLock: pass
def notify_if_ready(cls, deploy_id, fetch_complete=False): """ create activity and send deploy notifications if they haven't been sent """ from sentry.models import Activity, Environment, ReleaseCommit, ReleaseHeadCommit lock_key = cls.get_lock_key(deploy_id) lock = locks.get(lock_key, duration=30) with TimedRetryPolicy(10)(lock.acquire): deploy = cls.objects.filter( id=deploy_id, ).select_related('release').get() if deploy.notified: return release = deploy.release environment = Environment.objects.get( organization_id=deploy.organization_id, id=deploy.environment_id, ) if not fetch_complete: release_has_commits = ReleaseCommit.objects.filter( organization_id=release.organization_id, release=release, ).exists() if not release_has_commits: # check if we have head commits, which # would indicate that we're waiting for # fetch_commits to complete if ReleaseHeadCommit.objects.filter( organization_id=release.organization_id, release=release, ).exists(): return activity = None for project in deploy.release.projects.all(): activity = Activity.objects.create( type=Activity.DEPLOY, project=project, ident=release.version, data={ 'version': release.version, 'deploy_id': deploy.id, 'environment': environment.name, }, datetime=deploy.date_finished, ) # Somewhat hacky, only send notification for one # Deploy Activity record because it will cover all projects if activity is not None: activity.send_notification() deploy.update(notified=True)
def save(self, *args, **kwargs): if not self.slug: lock = locks.get("slug:project", duration=5) with TimedRetryPolicy(10)(lock.acquire): slugify_instance( self, self.name, organization=self.organization, reserved=RESERVED_PROJECT_SLUGS ) super(Project, self).save(*args, **kwargs) else: super(Project, self).save(*args, **kwargs) self.update_rev_for_option()
def get_webhook_secret(self, organization): lock = locks.get("bitbucket:webhook-secret:{}".format(organization.id), duration=60) with lock.acquire(): secret = OrganizationOption.objects.get_value( organization=organization, key="bitbucket:webhook_secret" ) if secret is None: secret = uuid4().hex + uuid4().hex OrganizationOption.objects.set_value( organization=organization, key="bitbucket:webhook_secret", value=secret ) return secret
def _locked_blob(checksum, logger=nooplogger): logger.debug("_locked_blob.start", extra={"checksum": checksum}) lock = locks.get("fileblob:upload:{}".format(checksum), duration=UPLOAD_RETRY_TIME) with TimedRetryPolicy(UPLOAD_RETRY_TIME, metric_instance="lock.fileblob.upload")(lock.acquire): logger.debug("_locked_blob.acquired", extra={"checksum": checksum}) # test for presence try: existing = FileBlob.objects.get(checksum=checksum) except FileBlob.DoesNotExist: existing = None yield existing logger.debug("_locked_blob.end", extra={"checksum": checksum})
def handle_group_owners(project, group, owners): """ Stores group owners generated by `ProjectOwnership.get_autoassign_owners` in the `GroupOwner` model, and handles any diffing/changes of which owners we're keeping. :return: """ from sentry.models.groupowner import GroupOwner, GroupOwnerType from sentry.models.team import Team from sentry.models.user import User lock = locks.get(f"groupowner-bulk:{group.id}", duration=10) try: with metrics.timer( "post_process.handle_group_owners"), sentry_sdk.start_span( op="post_process.handle_group_owners"), lock.acquire(): current_group_owners = GroupOwner.objects.filter( group=group, type=GroupOwnerType.OWNERSHIP_RULE.value) new_owners = {(type(owner), owner.id) for owner in owners} # Owners already in the database that we'll keep keeping_owners = set() for owner in current_group_owners: lookup_key = ((Team, owner.team_id) if owner.team_id is not None else (User, owner.user_id)) if lookup_key not in new_owners: owner.delete() else: keeping_owners.add(lookup_key) new_group_owners = [] for key in new_owners: if key not in keeping_owners: owner_type, owner_id = key user_id = None team_id = None if owner_type is User: user_id = owner_id if owner_type is Team: team_id = owner_id new_group_owners.append( GroupOwner( group=group, type=GroupOwnerType.OWNERSHIP_RULE.value, user_id=user_id, team_id=team_id, project=project, organization=project.organization, )) if new_group_owners: GroupOwner.objects.bulk_create(new_group_owners) except UnableToAcquireLock: pass
def get_or_create(cls, project, release, environment, datetime, **kwargs): cache_key = cls.get_cache_key(project.id, release.id, environment.id) instance = cache.get(cache_key) if instance is None: release_envs = list( cls.objects.filter( release_id=release.id, organization_id=project.organization_id, environment_id=environment.id, )) if release_envs: instance = release_envs[0] for re in release_envs: if re.project_id == project.id: instance = re created = False else: lock_key = cls.get_lock_key(project.organization_id, release.id, environment.id) lock = locks.get(lock_key, duration=5) with TimedRetryPolicy(10)(lock.acquire): try: instance, created = cls.objects.get( release_id=release.id, organization_id=project.organization_id, environment_id=environment.id, ), False except cls.DoesNotExist: instance, created = cls.objects.create( release_id=release.id, project_id=project.id, organization_id=project.organization_id, environment_id=environment.id, first_seen=datetime, last_seen=datetime, ), True cache.set(cache_key, instance, 3600) else: created = False # TODO(dcramer): this would be good to buffer, but until then we minimize # updates to once a minute, and allow Postgres to optimistically skip # it even if we can't if not created and instance.last_seen < datetime - timedelta( seconds=60): cls.objects.filter( id=instance.id, last_seen__lt=datetime - timedelta(seconds=60), ).update(last_seen=datetime, ) instance.last_seen = datetime cache.set(cache_key, instance, 3600) return instance
def _locked_blob(checksum, logger=nooplogger): logger.info('_locked_blob.start', extra={'checksum': checksum}) lock = locks.get(u'fileblob:upload:{}'.format(checksum), duration=UPLOAD_RETRY_TIME) with TimedRetryPolicy(UPLOAD_RETRY_TIME, metric_instance='lock.fileblob.upload')(lock.acquire): logger.info('_locked_blob.acquired', extra={'checksum': checksum}) # test for presence try: existing = FileBlob.objects.get(checksum=checksum) except FileBlob.DoesNotExist: existing = None yield existing logger.info('_locked_blob.end', extra={'checksum': checksum})
def get_or_create(cls, project, release, environment, datetime, **kwargs): cache_key = cls.get_cache_key(project.id, release.id, environment.id) instance = cache.get(cache_key) if instance is None: release_envs = list(cls.objects.filter( release_id=release.id, organization_id=project.organization_id, environment_id=environment.id, )) if release_envs: instance = release_envs[0] for re in release_envs: if re.project_id == project.id: instance = re created = False else: lock_key = cls.get_lock_key(project.organization_id, release.id, environment.id) lock = locks.get(lock_key, duration=5) with TimedRetryPolicy(10)(lock.acquire): try: instance, created = cls.objects.get( release_id=release.id, organization_id=project.organization_id, environment_id=environment.id, ), False except cls.DoesNotExist: instance, created = cls.objects.create( release_id=release.id, project_id=project.id, organization_id=project.organization_id, environment_id=environment.id, first_seen=datetime, last_seen=datetime, ), True cache.set(cache_key, instance, 3600) else: created = False # TODO(dcramer): this would be good to buffer, but until then we minimize # updates to once a minute, and allow Postgres to optimistically skip # it even if we can't if not created and instance.last_seen < datetime - timedelta(seconds=60): cls.objects.filter( id=instance.id, last_seen__lt=datetime - timedelta(seconds=60), ).update( last_seen=datetime, ) instance.last_seen = datetime cache.set(cache_key, instance, 3600) return instance
def get_webhook_secret(self, organization): # TODO(LB): Revisit whether Integrations V3 should be using OrganizationOption for storage lock = locks.get(u"bitbucket:webhook-secret:{}".format(organization.id), duration=60) with lock.acquire(): secret = OrganizationOption.objects.get_value( organization=organization, key="bitbucket:webhook_secret" ) if secret is None: secret = generate_token() OrganizationOption.objects.set_value( organization=organization, key="bitbucket:webhook_secret", value=secret ) return secret
def calculate_incident_suspects(incident_id): from sentry.incidents.logic import get_incident_suspect_commits lock = locks.get(u'incident:suspects:{}'.format(incident_id), duration=60 * 10) with TimedRetryPolicy(60)(lock.acquire): incident = Incident.objects.get(id=incident_id) suspect_commits = get_incident_suspect_commits(incident) with transaction.atomic(): IncidentSuspectCommit.objects.filter(incident=incident).delete() IncidentSuspectCommit.objects.bulk_create([ IncidentSuspectCommit(incident=incident, commit_id=commit_id, order=i) for i, commit_id in enumerate(suspect_commits) ])
def process_pending(): """ Process pending buffers. """ from sentry import buffer from sentry.app import locks lock = locks.get('buffer:process_pending', duration=60) try: with lock.acquire(): buffer.process_pending() except UnableToAcquireLock as error: logger.warning('process_pending.fail', extra={'error': error})
def get_webhook_secret(self, organization): lock = locks.get("github:webhook-secret:{}".format(organization.id), duration=60) with lock.acquire(): # TODO(dcramer): get_or_create would be a useful native solution secret = OrganizationOption.objects.get_value( organization=organization, key="github:webhook_secret" ) if secret is None: secret = uuid4().hex + uuid4().hex OrganizationOption.objects.set_value( organization=organization, key="github:webhook_secret", value=secret ) return secret
def save(self, *args, **kwargs): if not self.slug: lock = locks.get('slug:project', duration=5) with TimedRetryPolicy(10)(lock.acquire): slugify_instance( self, self.name, organization=self.organization, reserved=RESERVED_PROJECT_SLUGS) super(Project, self).save(*args, **kwargs) else: super(Project, self).save(*args, **kwargs) self.update_rev_for_option()
def upgrade(ctx, verbosity, traceback, noinput, lock, no_repair): "Perform any pending database migrations and upgrades." if lock: from sentry.app import locks from sentry.utils.locking import UnableToAcquireLock lock = locks.get('upgrade', duration=0) try: with lock.acquire(): _upgrade(not noinput, traceback, verbosity, not no_repair) except UnableToAcquireLock: raise click.ClickException('Unable to acquire `upgrade` lock.') else: _upgrade(not noinput, traceback, verbosity, not no_repair)
def enqueue_scheduled_jobs(**kwargs): from sentry.celery import app with locks.get('scheduler.process', duration=60).acquire(): job_list = list(ScheduledJob.objects.filter( date_scheduled__lte=timezone.now(), )[:101]) if len(job_list) > 100: logger.debug('More than 100 ScheduledJobs found.') for job in job_list: logger.debug('Sending scheduled job %s with payload %r', job.name, job.payload) app.send_task(job.name, kwargs=job.payload) job.delete()
def get_webhook_secret(self, organization): lock = locks.get('bitbucket:webhook-secret:{}'.format(organization.id), duration=60) with lock.acquire(): secret = OrganizationOption.objects.get_value( organization=organization, key='bitbucket:webhook_secret', ) if secret is None: secret = uuid4().hex + uuid4().hex OrganizationOption.objects.set_value( organization=organization, key='bitbucket:webhook_secret', value=secret, ) return secret
def enqueue_scheduled_jobs(**kwargs): from sentry.celery import app with locks.get("scheduler.process", duration=60).acquire(): job_list = list( ScheduledJob.objects.filter( date_scheduled__lte=timezone.now())[:101]) if len(job_list) > 100: logger.debug("More than 100 ScheduledJobs found.") for job in job_list: logger.debug("Sending scheduled job %s with payload %r", job.name, job.payload) app.send_task(job.name, kwargs=job.payload) job.delete()
def get_webhook_secret(self, organization): lock = locks.get('github:webhook-secret:{}'.format(organization.id), duration=60) with lock.acquire(): # TODO(dcramer): get_or_create would be a useful native solution secret = OrganizationOption.objects.get_value( organization=organization, key='github:webhook_secret', ) if secret is None: secret = uuid4().hex + uuid4().hex OrganizationOption.objects.set_value( organization=organization, key='github:webhook_secret', value=secret, ) return secret
def get_webhook_secret(self, organization): # TODO(LB): Revisit whether Integrations V3 should be using OrganizationOption for storage lock = locks.get('bitbucket:webhook-secret:{}'.format(organization.id), duration=60) with lock.acquire(): secret = OrganizationOption.objects.get_value( organization=organization, key='bitbucket:webhook_secret', ) if secret is None: secret = uuid4().hex + uuid4().hex OrganizationOption.objects.set_value( organization=organization, key='bitbucket:webhook_secret', value=secret, ) return secret
def get_webhook_secret(self, organization): # TODO(LB): Revisit whether Integrations V3 should be using OrganizationOption for storage lock = locks.get(u'bitbucket:webhook-secret:{}'.format(organization.id), duration=60) with lock.acquire(): secret = OrganizationOption.objects.get_value( organization=organization, key='bitbucket:webhook_secret', ) if secret is None: secret = generate_token() OrganizationOption.objects.set_value( organization=organization, key='bitbucket:webhook_secret', value=secret, ) return secret
def process_pending(partition=None): """ Process pending buffers. """ from sentry import buffer from sentry.app import locks if partition is None: lock_key = 'buffer:process_pending' else: lock_key = 'buffer:process_pending:%d' % partition lock = locks.get(lock_key, duration=60) try: with lock.acquire(): buffer.process_pending(partition=partition) except UnableToAcquireLock as error: logger.warning('process_pending.fail', extra={'error': error, 'partition': partition})
def from_file(cls, fileobj): """ Retrieve a list of FileBlobIndex instances for the given file. If not already present, this will cause it to be stored. >>> blobs = FileBlob.from_file(fileobj) """ size = 0 checksum = sha1(b'') for chunk in fileobj: size += len(chunk) checksum.update(chunk) checksum = checksum.hexdigest() # TODO(dcramer): the database here is safe, but if this lock expires # and duplicate files are uploaded then we need to prune one lock = locks.get(u'fileblob:upload:{}'.format(checksum), duration=60 * 10) with TimedRetryPolicy(60)(lock.acquire): # test for presence try: existing = FileBlob.objects.get(checksum=checksum) except FileBlob.DoesNotExist: pass else: return existing blob = cls( size=size, checksum=checksum, ) blob.path = cls.generate_unique_path(blob.timestamp) storage = get_storage() storage.save(blob.path, fileobj) blob.save() metrics.timing('filestore.blob-size', size) return blob
def finish_release(self, version, **values): values.setdefault('date_released', timezone.now()) affected = Release.objects.filter( version=version, organization_id=self.project.organization_id, projects=self.project, ).update(**values) if not affected: release = Release.objects.filter( version=version, organization_id=self.project.organization_id, ).first() if release: release.update(**values) else: lock_key = Release.get_lock_key(self.project.organization_id, version) lock = locks.get(lock_key, duration=5) with TimedRetryPolicy(10)(lock.acquire): try: release = Release.objects.get( version=version, organization_id=self.project.organization_id, ) except Release.DoesNotExist: release = Release.objects.create( version=version, organization_id=self.project.organization_id, **values ) release.add_project(self.project) activity = Activity.objects.create( type=Activity.RELEASE, project=self.project, ident=version, data={'version': version}, datetime=values['date_released'], ) activity.send_notification()
def get_or_create(cls, project, name): name = name or '' cache_key = cls.get_cache_key(project.id, name) env = cache.get(cache_key) if env is None: try: env = cls.objects.get( projects=project, organization_id=project.organization_id, name=name, ) except cls.DoesNotExist: env = cls.objects.filter( organization_id=project.organization_id, name=name, ).order_by('date_added').first() if not env: lock_key = cls.get_lock_key(project.organization_id, name) lock = locks.get(lock_key, duration=5) with TimedRetryPolicy(10)(lock.acquire): try: env = cls.objects.get( organization_id=project.organization_id, name=name, ) except cls.DoesNotExist: env = cls.objects.create( project_id=project.id, name=name, organization_id=project.organization_id ) env.add_project(project) cache.set(cache_key, env, 3600) return env
def post(self, request, project): """ Create a New Release ```````````````````` Create a new release and/or associate a project with a release. Release versions that are the same across multiple projects within an Organization will be treated as the same release in Sentry. Releases are used by Sentry to improve its error reporting abilities by correlating first seen events with the release that might have introduced the problem. Releases are also necessary for sourcemaps and other debug features that require manual upload for functioning well. :pparam string organization_slug: the slug of the organization the release belongs to. :pparam string project_slug: the slug of the project to create a release for. :param string version: a version identifier for this release. Can be a version number, a commit hash etc. :param string ref: an optional commit reference. This is useful if a tagged version has been provided. :param url url: a URL that points to the release. This can be the path to an online interface to the sourcecode for instance. :param datetime dateStarted: an optional date that indicates when the release process started. :param datetime dateReleased: an optional date that indicates when the release went live. If not provided the current time is assumed. :auth: required """ serializer = ReleaseSerializer(data=request.DATA) if serializer.is_valid(): result = serializer.object # release creation is idempotent to simplify user # experiences release = Release.objects.filter( organization_id=project.organization_id, version=result['version'], projects=project ).first() created = False if release: was_released = bool(release.date_released) else: release = Release.objects.filter( organization_id=project.organization_id, version=result['version'], ).first() if not release: lock_key = Release.get_lock_key(project.organization_id, result['version']) lock = locks.get(lock_key, duration=5) with TimedRetryPolicy(10)(lock.acquire): try: release, created = Release.objects.get( version=result['version'], organization_id=project.organization_id ), False except Release.DoesNotExist: release, created = Release.objects.create( organization_id=project.organization_id, version=result['version'], ref=result.get('ref'), url=result.get('url'), owner=result.get('owner'), date_started=result.get('dateStarted'), date_released=result.get('dateReleased'), ), True was_released = False try: with transaction.atomic(): ReleaseProject.objects.create(project=project, release=release) created = True except IntegrityError: pass commit_list = result.get('commits') if commit_list: hook = ReleaseHook(project) # TODO(dcramer): handle errors with release payloads hook.set_commits(release.version, commit_list) if (not was_released and release.date_released): activity = Activity.objects.create( type=Activity.RELEASE, project=project, ident=result['version'], data={'version': result['version']}, datetime=release.date_released, ) activity.send_notification() if not created: # This is the closest status code that makes sense, and we want # a unique 2xx response code so people can understand when # behavior differs. # 208 Already Reported (WebDAV; RFC 5842) status = 208 else: status = 201 return Response(serialize(release, request.user), status=status) return Response(serializer.errors, status=400)
def set_commits(self, version, commit_list): """ Commits should be ordered oldest to newest. Calling this method will remove all existing commit history. """ project = self.project release = Release.objects.filter( organization_id=project.organization_id, version=version, projects=self.project ).first() if not release: release = Release.objects.filter( organization_id=project.organization_id, version=version, ).first() if not release: lock_key = Release.get_lock_key(project.organization_id, version) lock = locks.get(lock_key, duration=5) with TimedRetryPolicy(10)(lock.acquire): try: release = Release.objects.get( organization_id=project.organization_id, version=version ) except Release.DoesNotExist: release = Release.objects.create( organization_id=project.organization_id, version=version ) release.add_project(project) with transaction.atomic(): # TODO(dcramer): would be good to optimize the logic to avoid these # deletes but not overly important ReleaseCommit.objects.filter( release=release, ).delete() authors = {} repos = {} for idx, data in enumerate(commit_list): repo_name = data.get('repository') or 'project-{}'.format(project.id) if repo_name not in repos: repos[repo_name] = repo = Repository.objects.get_or_create( organization_id=project.organization_id, name=repo_name, )[0] else: repo = repos[repo_name] author_email = data.get('author_email') if author_email is None and data.get('author_name'): author_email = self._to_email(data['author_name']) if not author_email: author = None elif author_email not in authors: authors[author_email] = author = CommitAuthor.objects.get_or_create( organization_id=project.organization_id, email=author_email, defaults={ 'name': data.get('author_name'), } )[0] if data.get('author_name') and author.name != data['author_name']: author.update(name=data['author_name']) else: author = authors[author_email] commit = Commit.objects.get_or_create( organization_id=project.organization_id, repository_id=repo.id, key=data['id'], defaults={ 'message': data.get('message'), 'author': author, 'date_added': data.get('timestamp') or timezone.now(), } )[0] ReleaseCommit.objects.create( organization_id=project.organization_id, release=release, commit=commit, order=idx, )
def post(self, request, organization, version): """ Create a Deploy ``````````````` Create a deploy for a given release. :pparam string organization_slug: the organization short name :pparam string version: the version identifier of the release. :param string environment: the environment you're deploying to :param string name: the optional name of the deploy :param url url: the optional url that points to the deploy :param datetime dateStarted: an optional date that indicates when the deploy started :param datetime dateFinished: an optional date that indicates when the deploy ended. If not provided, the current time is used. """ try: release = Release.objects.get( version=version, organization=organization, ) except Release.DoesNotExist: raise ResourceDoesNotExist if not self.has_release_permission(request, organization, release): raise PermissionDenied serializer = DeploySerializer(data=request.DATA) if serializer.is_valid(): result = serializer.object try: env = Environment.objects.get( organization_id=organization.id, name=result['environment'], ) except Environment.DoesNotExist: # TODO(jess): clean up when changing unique constraint lock_key = Environment.get_lock_key(organization.id, result['environment']) lock = locks.get(lock_key, duration=5) with TimedRetryPolicy(10)(lock.acquire): try: env = Environment.objects.get( organization_id=organization.id, name=result['environment'], ) except Environment.DoesNotExist: env = Environment.objects.create( organization_id=organization.id, name=result['environment'], ) try: with transaction.atomic(): deploy, created = Deploy.objects.create( organization_id=organization.id, release=release, environment_id=env.id, date_finished=result.get('dateFinished', timezone.now()), date_started=result.get('dateStarted'), name=result.get('name'), url=result.get('url'), ), True except IntegrityError: deploy, created = Deploy.objects.get( organization_id=organization.id, release=release, environment_id=env.id, ), False deploy.update( date_finished=result.get('dateFinished', timezone.now()), date_started=result.get('dateStarted'), ) activity = None for project in release.projects.all(): activity = Activity.objects.create( type=Activity.DEPLOY, project=project, ident=release.version, data={ 'version': release.version, 'deploy_id': deploy.id, 'environment': env.name }, datetime=deploy.date_finished, ) # Somewhat hacky, only send notification for one # Deploy Activity record because it will cover all projects if activity is not None: activity.send_notification() # This is the closest status code that makes sense, and we want # a unique 2xx response code so people can understand when # behavior differs. # 208 Already Reported (WebDAV; RFC 5842) status = 201 if created else 208 return Response(serialize(deploy, request.user), status=status) return Response(serializer.errors, status=400)
def delete(self, *args, **kwargs): lock = locks.get(u'fileblob:upload:{}'.format(self.checksum), duration=60 * 10) with TimedRetryPolicy(60)(lock.acquire): super(FileBlob, self).delete(*args, **kwargs) if self.path: self.deletefile(commit=False)
def set_commits(self, commit_list): """ Bind a list of commits to this release. This will clear any existing commit log and replace it with the given commits. """ # Sort commit list in reverse order commit_list.sort(key=lambda commit: commit.get('timestamp'), reverse=True) # TODO(dcramer): this function could use some cleanup/refactoring as its a bit unwieldly from sentry.models import ( Commit, CommitAuthor, Group, GroupLink, GroupResolution, GroupStatus, ReleaseCommit, ReleaseHeadCommit, Repository, PullRequest ) from sentry.plugins.providers.repository import RepositoryProvider # todo(meredith): implement for IntegrationRepositoryProvider commit_list = [ c for c in commit_list if not RepositoryProvider.should_ignore_commit(c.get('message', '')) ] lock_key = type(self).get_lock_key(self.organization_id, self.id) lock = locks.get(lock_key, duration=10) with TimedRetryPolicy(10)(lock.acquire): with transaction.atomic(): # TODO(dcramer): would be good to optimize the logic to avoid these # deletes but not overly important ReleaseCommit.objects.filter( release=self, ).delete() authors = {} repos = {} commit_author_by_commit = {} head_commit_by_repo = {} latest_commit = None for idx, data in enumerate(commit_list): repo_name = data.get('repository' ) or 'organization-{}'.format(self.organization_id) if repo_name not in repos: repos[repo_name] = repo = Repository.objects.get_or_create( organization_id=self.organization_id, name=repo_name, )[0] else: repo = repos[repo_name] author_email = data.get('author_email') if author_email is None and data.get('author_name'): author_email = ( re.sub(r'[^a-zA-Z0-9\-_\.]*', '', data['author_name']).lower() + '@localhost' ) if not author_email: author = None elif author_email not in authors: authors[author_email] = author = CommitAuthor.objects.get_or_create( organization_id=self.organization_id, email=author_email, defaults={ 'name': data.get('author_name'), } )[0] if data.get('author_name') and author.name != data['author_name']: author.update(name=data['author_name']) else: author = authors[author_email] defaults = { 'message': data.get('message'), 'author': author, 'date_added': data.get('timestamp') or timezone.now(), } commit, created = Commit.objects.get_or_create( organization_id=self.organization_id, repository_id=repo.id, key=data['id'], defaults=defaults, ) if author is None: author = commit.author commit_author_by_commit[commit.id] = author patch_set = data.get('patch_set', []) for patched_file in patch_set: CommitFileChange.objects.get_or_create( organization_id=self.organization.id, commit=commit, filename=patched_file['path'], type=patched_file['type'], ) if not created: update_kwargs = {} if commit.message is None and defaults['message'] is not None: update_kwargs['message'] = defaults['message'] if commit.author_id is None and defaults['author'] is not None: update_kwargs['author'] = defaults['author'] if update_kwargs: commit.update(**update_kwargs) ReleaseCommit.objects.create( organization_id=self.organization_id, release=self, commit=commit, order=idx, ) if latest_commit is None: latest_commit = commit head_commit_by_repo.setdefault(repo.id, commit.id) self.update( commit_count=len(commit_list), authors=[ six.text_type(a_id) for a_id in ReleaseCommit.objects.filter( release=self, commit__author_id__isnull=False, ).values_list('commit__author_id', flat=True).distinct() ], last_commit_id=latest_commit.id if latest_commit else None, ) # fill any missing ReleaseHeadCommit entries for repo_id, commit_id in six.iteritems(head_commit_by_repo): try: with transaction.atomic(): ReleaseHeadCommit.objects.create( organization_id=self.organization_id, release_id=self.id, repository_id=repo_id, commit_id=commit_id, ) except IntegrityError: pass release_commits = list(ReleaseCommit.objects.filter(release=self) .select_related('commit').values('commit_id', 'commit__key')) commit_resolutions = list( GroupLink.objects.filter( linked_type=GroupLink.LinkedType.commit, linked_id__in=[rc['commit_id'] for rc in release_commits], ).values_list('group_id', 'linked_id') ) commit_group_authors = [ (cr[0], # group_id commit_author_by_commit.get(cr[1])) for cr in commit_resolutions] pr_ids_by_merge_commit = list(PullRequest.objects.filter( merge_commit_sha__in=[rc['commit__key'] for rc in release_commits], organization_id=self.organization_id, ).values_list('id', flat=True)) pull_request_resolutions = list( GroupLink.objects.filter( relationship=GroupLink.Relationship.resolves, linked_type=GroupLink.LinkedType.pull_request, linked_id__in=pr_ids_by_merge_commit, ).values_list('group_id', 'linked_id') ) pr_authors = list(PullRequest.objects.filter( id__in=[prr[1] for prr in pull_request_resolutions], ).select_related('author')) pr_authors_dict = { pra.id: pra.author for pra in pr_authors } pull_request_group_authors = [(prr[0], pr_authors_dict.get(prr[1])) for prr in pull_request_resolutions] user_by_author = {None: None} for group_id, author in itertools.chain(commit_group_authors, pull_request_group_authors): if author not in user_by_author: try: user_by_author[author] = author.find_users()[0] except IndexError: user_by_author[author] = None actor = user_by_author[author] with transaction.atomic(): GroupResolution.objects.create_or_update( group_id=group_id, values={ 'release': self, 'type': GroupResolution.Type.in_release, 'status': GroupResolution.Status.resolved, 'actor_id': actor.id if actor else None, }, ) Group.objects.filter( id=group_id, ).update(status=GroupStatus.RESOLVED)
def post(self, request, organization): """ Add a Member to Organization ```````````````````````````` Invite a member to the organization. :pparam string organization_slug: the slug of the organization the member will belong to :param string email: the email address to invite :param string role: the role of the new member :param array teams: the slugs of the teams the member should belong to. :auth: required """ # TODO: If the member already exists, should this still update the role and team? # For now, it doesn't, but simply returns the existing object if not features.has('organizations:invite-members', organization, actor=request.user): return Response( {'organization': 'Your organization is not allowed to invite members'}, status=403) serializer = OrganizationMemberSerializer(data=request.DATA) if not serializer.is_valid(): return Response(serializer.errors, status=400) result = serializer.object _, allowed_roles = get_allowed_roles(request, organization) # ensure listed teams are real teams teams = list(Team.objects.filter( organization=organization, status=TeamStatus.VISIBLE, slug__in=result['teams'], )) if len(set(result['teams'])) != len(teams): return Response({'teams': 'Invalid team'}, 400) if not result['role'] in {r.id for r in allowed_roles}: return Response({'role': 'You do not have permission to invite that role.'}, 403) # This is needed because `email` field is case sensitive, but from a user perspective, # Sentry treats email as case-insensitive ([email protected] equals [email protected]). existing = OrganizationMember.objects.filter( organization=organization, user__email__iexact=result['email'], user__is_active=True, ).exists() if existing: return Response({'email': 'The user %s is already a member' % result['email']}, 409) om = OrganizationMember( organization=organization, email=result['email'], role=result['role']) if settings.SENTRY_ENABLE_INVITES: om.token = om.generate_token() try: with transaction.atomic(): om.save() except IntegrityError: return Response({'email': 'The user %s is already a member' % result['email']}, 409) lock = locks.get(u'org:member:{}'.format(om.id), duration=5) with TimedRetryPolicy(10)(lock.acquire): self.save_team_assignments(om, teams) if settings.SENTRY_ENABLE_INVITES: om.send_invite_email() member_invited.send_robust(member=om, user=request.user, sender=self, referrer=request.DATA.get('referrer')) self.create_audit_entry( request=request, organization_id=organization.id, target_object=om.id, data=om.get_audit_log_data(), event=AuditLogEntryEvent.MEMBER_INVITE if settings.SENTRY_ENABLE_INVITES else AuditLogEntryEvent.MEMBER_ADD, ) return Response(serialize(om), status=201)
def set_commits(self, commit_list): """ Bind a list of commits to this release. These should be ordered from newest to oldest. This will clear any existing commit log and replace it with the given commits. """ from sentry.models import ( Commit, CommitAuthor, Group, GroupLink, GroupResolution, GroupStatus, ReleaseCommit, Repository ) from sentry.plugins.providers.repository import RepositoryProvider commit_list = [ c for c in commit_list if not RepositoryProvider.should_ignore_commit(c.get('message', '')) ] lock_key = type(self).get_lock_key(self.organization_id, self.id) lock = locks.get(lock_key, duration=10) with TimedRetryPolicy(10)(lock.acquire): with transaction.atomic(): # TODO(dcramer): would be good to optimize the logic to avoid these # deletes but not overly important ReleaseCommit.objects.filter( release=self, ).delete() authors = {} repos = {} commit_author_by_commit = {} latest_commit = None for idx, data in enumerate(commit_list): repo_name = data.get('repository' ) or 'organization-{}'.format(self.organization_id) if repo_name not in repos: repos[repo_name] = repo = Repository.objects.get_or_create( organization_id=self.organization_id, name=repo_name, )[0] else: repo = repos[repo_name] author_email = data.get('author_email') if author_email is None and data.get('author_name'): author_email = ( re.sub(r'[^a-zA-Z0-9\-_\.]*', '', data['author_name']).lower() + '@localhost' ) if not author_email: author = None elif author_email not in authors: authors[author_email] = author = CommitAuthor.objects.get_or_create( organization_id=self.organization_id, email=author_email, defaults={ 'name': data.get('author_name'), } )[0] if data.get('author_name') and author.name != data['author_name']: author.update(name=data['author_name']) else: author = authors[author_email] defaults = { 'message': data.get('message'), 'author': author, 'date_added': data.get('timestamp') or timezone.now(), } commit, created = Commit.objects.get_or_create( organization_id=self.organization_id, repository_id=repo.id, key=data['id'], defaults=defaults, ) if author is None: author = commit.author commit_author_by_commit[commit.id] = author patch_set = data.get('patch_set', []) for patched_file in patch_set: CommitFileChange.objects.get_or_create( organization_id=self.organization.id, commit=commit, filename=patched_file['path'], type=patched_file['type'], ) if not created: update_kwargs = {} if commit.message is None and defaults['message'] is not None: update_kwargs['message'] = defaults['message'] if commit.author_id is None and defaults['author'] is not None: update_kwargs['author'] = defaults['author'] if update_kwargs: commit.update(**update_kwargs) ReleaseCommit.objects.create( organization_id=self.organization_id, release=self, commit=commit, order=idx, ) if latest_commit is None: latest_commit = commit self.update( commit_count=len(commit_list), authors=[ six.text_type(a_id) for a_id in ReleaseCommit.objects.filter( release=self, commit__author_id__isnull=False, ).values_list('commit__author_id', flat=True).distinct() ], last_commit_id=latest_commit.id if latest_commit else None, ) commit_resolutions = list( GroupLink.objects.filter( linked_type=GroupLink.LinkedType.commit, linked_id__in=ReleaseCommit.objects.filter(release=self) .values_list('commit_id', flat=True), ).values_list('group_id', 'linked_id') ) user_by_author = {None: None} for group_id, linked_id in commit_resolutions: author = commit_author_by_commit.get(linked_id) if author not in user_by_author: try: user_by_author[author] = author.find_users()[0] except IndexError: user_by_author[author] = None actor = user_by_author[author] with transaction.atomic(): GroupResolution.objects.create_or_update( group_id=group_id, values={ 'release': self, 'type': GroupResolution.Type.in_release, 'status': GroupResolution.Status.resolved, 'actor_id': actor.id if actor else None, }, ) Group.objects.filter( id=group_id, ).update(status=GroupStatus.RESOLVED)