Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backup): Send relocation started email #59922

Merged
merged 1 commit into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions src/sentry/tasks/relocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
import yaml
from cryptography.fernet import Fernet
from django.db import router, transaction
from django.utils import timezone as get_timezone
from google.cloud.devtools.cloudbuild_v1 import Build
from google.cloud.devtools.cloudbuild_v1 import CloudBuildClient as CloudBuildClient

from sentry import options
from sentry.api.serializers.rest_framework.base import camel_to_snake_case, convert_dict_key_case
from sentry.backup.dependencies import NormalizedModelName, get_model
from sentry.backup.exports import export_in_config_scope, export_in_user_scope
Expand All @@ -26,6 +28,7 @@
)
from sentry.backup.imports import import_in_organization_scope
from sentry.filestore.gcs import GoogleCloudStorage
from sentry.http import get_server_hostname
from sentry.models.files.file import File
from sentry.models.files.utils import get_storage
from sentry.models.importchunk import RegionImportChunk
Expand All @@ -39,10 +42,12 @@
)
from sentry.models.user import User
from sentry.services.hybrid_cloud.organization import organization_service
from sentry.services.hybrid_cloud.user.service import user_service
from sentry.silo import SiloMode
from sentry.tasks.base import instrumented_task
from sentry.utils import json
from sentry.utils.db import atomic_transaction
from sentry.utils.email.message_builder import MessageBuilder
from sentry.utils.env import gcp_project_id
from sentry.utils.relocation import (
RELOCATION_BLOB_SIZE,
Expand Down Expand Up @@ -307,11 +312,35 @@ def preprocessing_scan(uuid: str) -> None:
relocation.want_usernames = sorted(usernames)
relocation.save()

# TODO(getsentry/team-ospo#203): The user's import data looks basically okay - we should
# use this opportunity to send a "your relocation request has been accepted and is in
# flight, please give it a couple hours" email.
preprocessing_baseline_config.delay(uuid)

# The user's import data looks basically okay - we can use this opportunity to send a
# "your relocation request has been accepted and is in flight, please give it a few
# hours" email.
msg = MessageBuilder(
subject=f"{options.get('mail.subject-prefix')} Your Relocation has Started",
template="sentry/emails/relocation-started.txt",
html_template="sentry/emails/relocation-started.html",
type="relocation.started",
context={
"domain": get_server_hostname(),
"datetime": get_timezone.now(),
"uuid": str(relocation.uuid),
"orgs": relocation.want_org_slugs,
},
)
email_to = []
owner = user_service.get_user(user_id=relocation.owner_id)
if owner is not None:
email_to.append(owner.email)

if relocation.owner_id != relocation.creator_id:
creator = user_service.get_user(user_id=relocation.creator_id)
if creator is not None:
email_to.append(creator.email)

msg.send_async(to=email_to)


@instrumented_task(
name="sentry.relocation.preprocessing_baseline_config",
Expand Down
5 changes: 4 additions & 1 deletion src/sentry/templates/sentry/debug/mail/preview.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
<optgroup label="Account">
<option value="mail/confirm-email/">Confirm Email</option>
<option value="mail/recover-account/">Reset Password</option>
<option value="mail/relocate-account/">Relocate Account</option>
<option value="mail/invalid-identity/">Invalid Identity</option>
<option value="mail/sso-linked/">SSO Linked</option>
<option value="mail/sso-unlinked/">SSO Unlinked</option>
Expand All @@ -53,6 +52,10 @@
<option value="mail/sentry-app-notify-disable/">Sentry App Notify Disable</option>
<option value="mail/join-request/">Join Request</option>
</optgroup>
<optgroup label="Relocation">
<option value="mail/relocate-account/">Relocate Account</option>
<option value="mail/relocation-started/">Relocation Started</option>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we move Relocate Account here if this is now a new group?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

</optgroup>
<optgroup label="Reports">
<option value="mail/weekly-reports/">Weekly Report</option>
</optgroup>
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/templates/sentry/emails/relocate_account.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
The following Sentry organizations that you are a member of have been migrated onto sentry.io:
{% for org in orgs %}
{{ org }}
* {% endfor %}
* {{ org }}
{% endfor %}

To continue with using these accounts at their new location, please claim your account with sentry.io.

Expand Down
17 changes: 17 additions & 0 deletions src/sentry/templates/sentry/emails/relocation_started.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% extends "sentry/emails/base.html" %}

{% load i18n %}

{% block main %}
<h3>Relocation Accepted</h3>
<p>Your relocation request has been accepted. You requested that the following organizations be migrating to sentry.io:</p>
<ul>
{% for org in orgs %}
<li>
<a>{{ org }}</a>
</li>
{% endfor %}
</ul>
<p>Relocations usually complete in 24 hours or less. If you do not hear from us in that time frame, please <a href="https://help.sentry.io">contact support</a>.</p>
<p><small><i>ID: {{ uuid }}</i></small></p>
{% endblock %}
9 changes: 9 additions & 0 deletions src/sentry/templates/sentry/emails/relocation_started.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Your relocation request has been accepted. You requested that the following organizations be migrating to sentry.io:

{% for org in orgs %}
* {{ org }}
{% endfor %}

Relocations usually complete in 24 hours or less. If you do not hear from us in that time frame, please contact support at https://help.sentry.io.

ID: {{ uuid }}
1 change: 1 addition & 0 deletions src/sentry/web/debug_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
re_path(r"^debug/mail/confirm-email/$", sentry.web.frontend.debug.mail.confirm_email),
re_path(r"^debug/mail/recover-account/$", sentry.web.frontend.debug.mail.recover_account),
re_path(r"^debug/mail/relocate-account/$", sentry.web.frontend.debug.mail.relocate_account),
re_path(r"^debug/mail/relocation-started/$", sentry.web.frontend.debug.mail.relocation_started),
re_path(r"^debug/mail/unable-to-delete-repo/$", DebugUnableToDeleteRepository.as_view()),
re_path(r"^debug/mail/unable-to-fetch-commits/$", DebugUnableToFetchCommitsEmailView.as_view()),
re_path(r"^debug/mail/unassigned/$", DebugUnassignedEmailView.as_view()),
Expand Down
14 changes: 14 additions & 0 deletions src/sentry/web/frontend/debug/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,20 @@ def relocate_account(request):
).render(request)


@login_required
def relocation_started(request):
return MailPreview(
html_template="sentry/emails/relocation_started.html",
text_template="sentry/emails/relocation_started.txt",
context={
"domain": get_server_hostname(),
"datetime": timezone.now(),
"uuid": str(uuid.uuid4().hex),
"orgs": ["testsentry", "testgetsentry"],
},
).render(request)


@login_required
def org_delete_confirm(request):
from sentry.models.auditlogentry import AuditLogEntry
Expand Down
37 changes: 33 additions & 4 deletions tests/sentry/tasks/test_relocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ class RelocationTaskTestCase(TestCase):
def setUp(self):
super().setUp()
self.owner = self.create_user(
email="owner", is_superuser=False, is_staff=False, is_active=True
email="owner@example.com", is_superuser=False, is_staff=False, is_active=True
)
self.superuser = self.create_user(
"superuser", is_superuser=True, is_staff=True, is_active=True
email="superuser@example.com", is_superuser=True, is_staff=True, is_active=True
)
self.login_as(user=self.superuser, superuser=True)
self.relocation: Relocation = Relocation.objects.create(
Expand Down Expand Up @@ -231,8 +231,10 @@ def setUp(self):
self.relocation.latest_task = "UPLOADING_COMPLETE"
self.relocation.save()

def test_success(
@patch("sentry.utils.email.MessageBuilder.send_async")
def test_success_admin_assisted_relocation(
self,
send_async_mock: Mock,
preprocessing_baseline_config_mock: Mock,
fake_kms_client: FakeKeyManagementServiceClient,
):
Expand All @@ -243,6 +245,26 @@ def test_success(
assert fake_kms_client.asymmetric_decrypt.call_count == 1
assert fake_kms_client.get_public_key.call_count == 0
assert preprocessing_baseline_config_mock.call_count == 1
send_async_mock.assert_called_once_with(to=[self.owner.email, self.superuser.email])
assert Relocation.objects.get(uuid=self.uuid).want_usernames == ["[email protected]"]

@patch("sentry.utils.email.MessageBuilder.send_async")
def test_success_self_service_relocation(
self,
send_async_mock: Mock,
preprocessing_baseline_config_mock: Mock,
fake_kms_client: FakeKeyManagementServiceClient,
):
self.mock_kms_client(fake_kms_client)
self.relocation.creator_id = self.relocation.owner_id
self.relocation.save()

preprocessing_scan(self.uuid)

assert fake_kms_client.asymmetric_decrypt.call_count == 1
assert fake_kms_client.get_public_key.call_count == 0
assert preprocessing_baseline_config_mock.call_count == 1
send_async_mock.assert_called_once_with(to=[self.owner.email])
assert Relocation.objects.get(uuid=self.uuid).want_usernames == ["[email protected]"]

def test_retry_if_attempts_left(
Expand Down Expand Up @@ -462,7 +484,7 @@ def test_success(
# Only user `superuser` is an admin, so only they should be exported.
for json_model in json_models:
if NormalizedModelName(json_model["model"]) == get_model_name(User):
assert json_model["fields"]["username"] in "superuser"
assert json_model["fields"]["username"] in "superuser@example.com"

def test_retry_if_attempts_left(
self,
Expand Down Expand Up @@ -1227,6 +1249,7 @@ def test_fail_if_no_attempts_left(self, completed_mock: Mock):
"sentry.tasks.relocation.CloudBuildClient",
new_callable=lambda: FakeCloudBuildClient,
)
@patch("sentry.utils.email.MessageBuilder.send_async")
@region_silo_test
class EndToEndTest(RelocationTaskTestCase, TransactionTestCase):
def setUp(self):
Expand All @@ -1252,6 +1275,7 @@ def setUp(self):

def test_valid_no_retries(
self,
send_async_mock: Mock,
fake_cloudbuild_client: FakeCloudBuildClient,
fake_kms_client: FakeKeyManagementServiceClient,
):
Expand Down Expand Up @@ -1287,8 +1311,11 @@ def test_valid_no_retries(
"sentry.useremail",
]

assert send_async_mock.call_count == 1

def test_invalid_no_retries(
self,
send_async_mock: Mock,
fake_cloudbuild_client: FakeCloudBuildClient,
fake_kms_client: FakeKeyManagementServiceClient,
):
Expand All @@ -1305,6 +1332,8 @@ def test_invalid_no_retries(
assert relocation.failure_reason
assert Organization.objects.filter(slug__startswith="testing").count() == org_count

assert send_async_mock.call_count == 1

# TODO(getsentry/team-ospo#190): We should add "max retry" tests as well, but these are quite
# hard to mock in celery at the moment. We may need to use the mock sync celery test scheduler,
# rather than the "self.tasks()" approach above, to accomplish this.
Loading