33from typing import BinaryIO , Iterator , Optional , Tuple , Type
44
55import click
6- from django .conf import settings
76from django .core import serializers
8- from django .core .exceptions import ValidationError as DjangoValidationError
9- from django .db import IntegrityError , connections , router , transaction
7+ from django .db import transaction
108from django .db .models .base import Model
11- from rest_framework .serializers import ValidationError as DjangoRestFrameworkValidationError
129
13- from sentry .backup .dependencies import NormalizedModelName , PrimaryKeyMap , get_model , get_model_name
14- from sentry .backup .helpers import EXCLUDED_APPS , Filter , ImportFlags , decrypt_encrypted_tarball
10+ from sentry .backup .dependencies import (
11+ NormalizedModelName ,
12+ PrimaryKeyMap ,
13+ dependencies ,
14+ get_model_name ,
15+ )
16+ from sentry .backup .helpers import Filter , ImportFlags , decrypt_encrypted_tarball
1517from sentry .backup .scopes import ImportScope
16- from sentry .silo import unguarded_write
18+ from sentry .services .hybrid_cloud .import_export .model import (
19+ RpcFilter ,
20+ RpcImportError ,
21+ RpcImportErrorKind ,
22+ RpcImportFlags ,
23+ RpcImportScope ,
24+ RpcPrimaryKeyMap ,
25+ )
26+ from sentry .services .hybrid_cloud .import_export .service import (
27+ ImportExportService ,
28+ import_export_service ,
29+ )
30+ from sentry .silo .base import SiloMode
31+ from sentry .silo .safety import unguarded_write
1732from sentry .utils import json
33+ from sentry .utils .env import is_split_db
1834
1935__all__ = (
36+ "ImportingError" ,
2037 "import_in_user_scope" ,
2138 "import_in_organization_scope" ,
2239 "import_in_config_scope" ,
2340 "import_in_global_scope" ,
2441)
2542
2643
44+ class ImportingError (Exception ):
45+ def __init__ (self , context : RpcImportError ) -> None :
46+ self .context = context
47+
48+
2749def _import (
2850 src : BinaryIO ,
2951 scope : ImportScope ,
@@ -45,6 +67,11 @@ def _import(
4567 from sentry .models .organizationmember import OrganizationMember
4668 from sentry .models .user import User
4769
70+ if SiloMode .get_current_mode () == SiloMode .CONTROL :
71+ errText = "Imports must be run in REGION or MONOLITH instances only"
72+ printer (errText , err = True )
73+ raise RuntimeError (errText )
74+
4875 flags = flags if flags is not None else ImportFlags ()
4976 user_model_name = get_model_name (User )
5077 org_model_name = get_model_name (Organization )
@@ -150,87 +177,47 @@ def yield_json_models(src) -> Iterator[Tuple[NormalizedModelName, str]]:
150177 if last_seen_model_name is not None and batch :
151178 yield (last_seen_model_name , json .dumps (batch ))
152179
180+ def get_importer_for_model (model : Type [Model ]):
181+ if SiloMode .CONTROL in model ._meta .silo_limit .modes : # type: ignore
182+ return import_export_service .import_by_model
183+ return ImportExportService .get_local_implementation ().import_by_model # type: ignore
184+
153185 # Extract some write logic into its own internal function, so that we may call it irrespective
154186 # of how we do atomicity: on a per-model (if using multiple dbs) or global (if using a single
155187 # db) basis.
156188 def do_write ():
157- allowed_relocation_scopes = scope .value
158189 pk_map = PrimaryKeyMap ()
159- for (batch_model_name , batch ) in yield_json_models (src ):
160- model = get_model (batch_model_name )
161- if model is None :
162- raise ValueError ("Unknown model name" )
163-
164- using = router .db_for_write (model )
165- with transaction .atomic (using = using ):
166- count = 0
167- for obj in serializers .deserialize ("json" , batch , use_natural_keys = False ):
168- o = obj .object
169- if o ._meta .app_label not in EXCLUDED_APPS or o :
170- if o .get_possible_relocation_scopes () & allowed_relocation_scopes :
171- o = obj .object
172- model_name = get_model_name (o )
173- for f in filters :
174- if f .model == type (o ) and getattr (o , f .field , None ) not in f .values :
175- break
176- else :
177- # We can only be sure `get_relocation_scope()` will be correct if it
178- # is fired AFTER normalization, as some `get_relocation_scope()`
179- # methods rely on being able to correctly resolve foreign keys,
180- # which is only possible after normalization.
181- old_pk = o .normalize_before_relocation_import (pk_map , scope , flags )
182- if old_pk is None :
183- continue
184-
185- # Now that the model has been normalized, we can ensure that this
186- # particular instance has a `RelocationScope` that permits
187- # importing.
188- if not o .get_relocation_scope () in allowed_relocation_scopes :
189- continue
190-
191- written = o .write_relocation_import (scope , flags )
192- if written is None :
193- continue
194-
195- new_pk , import_kind = written
196- slug = getattr (o , "slug" , None )
197- pk_map .insert (model_name , old_pk , new_pk , import_kind , slug )
198- count += 1
199-
200- # If we wrote at least one model, make sure to update the sequences too.
201- if count > 0 :
202- table = o ._meta .db_table
203- seq = f"{ table } _id_seq"
204- with connections [using ].cursor () as cursor :
205- cursor .execute (f"SELECT setval(%s, (SELECT MAX(id) FROM { table } ))" , [seq ])
206-
207- try :
208- if len (settings .DATABASES ) == 1 :
209- # TODO(getsentry/team-ospo#185): This is currently untested in single-db mode. Fix ASAP!
210- with unguarded_write (using = "default" ), transaction .atomic ("default" ):
211- do_write ()
212- else :
190+ for model_name , json_data in yield_json_models (src ):
191+ model_relations = dependencies ().get (model_name )
192+ if not model_relations :
193+ continue
194+
195+ dep_models = {
196+ get_model_name (d ) for d in model_relations .get_dependencies_for_relocation ()
197+ }
198+ import_by_model = get_importer_for_model (model_relations .model )
199+ result = import_by_model (
200+ model_name = str (model_name ),
201+ scope = RpcImportScope .into_rpc (scope ),
202+ flags = RpcImportFlags .into_rpc (flags ),
203+ filter_by = [RpcFilter .into_rpc (f ) for f in filters ],
204+ pk_map = RpcPrimaryKeyMap .into_rpc (pk_map .partition (dep_models )),
205+ json_data = json_data ,
206+ )
207+
208+ if isinstance (result , RpcImportError ):
209+ printer (result .pretty (), err = True )
210+ if result .get_kind () == RpcImportErrorKind .IntegrityError :
211+ warningText = ">> Are you restoring from a backup of the same version of Sentry?\n >> Are you restoring onto a clean database?\n >> If so then this IntegrityError might be our fault, you can open an issue here:\n >> https://github.com/getsentry/sentry/issues/new/choose"
212+ printer (warningText , err = True )
213+ raise ImportingError (result )
214+ pk_map .extend (result .mapped_pks )
215+
216+ if SiloMode .get_current_mode () == SiloMode .MONOLITH and not is_split_db ():
217+ with unguarded_write (using = "default" ), transaction .atomic (using = "default" ):
213218 do_write ()
214-
215- # For all database integrity errors, let's warn users to follow our
216- # recommended backup/restore workflow before reraising exception. Most of
217- # these errors come from restoring on a different version of Sentry or not restoring
218- # on a clean install.
219- except IntegrityError as e :
220- warningText = ">> Are you restoring from a backup of the same version of Sentry?\n >> Are you restoring onto a clean database?\n >> If so then this IntegrityError might be our fault, you can open an issue here:\n >> https://github.com/getsentry/sentry/issues/new/choose"
221- printer (
222- warningText ,
223- err = True ,
224- )
225- raise (e )
226-
227- # Calls to `write_relocation_import` may fail validation and throw either a
228- # `DjangoValidationError` when a call to `.full_clean()` failed, or a
229- # `DjangoRestFrameworkValidationError` when a call to a custom DRF serializer failed. This
230- # exception catcher converts instances of the former to the latter.
231- except DjangoValidationError as e :
232- errs = {field : error for field , error in e .message_dict .items ()}
233- raise DjangoRestFrameworkValidationError (errs ) from e
219+ else :
220+ do_write ()
234221
235222
236223def import_in_user_scope (
0 commit comments