db: Migrate "service" APIs to enginefacade

Migrate service-related APIs from the legacy enginefacade to the modern
context-based enginefacade. This one is a little tricky as it highlights
two issues.

* The models in 'manila.db.sqlalchemy.models' do not match those created
  by the migrations. This causes us issues since we apply the former
  rather than running migrations when creating our database via the
  DatabaseFixture, which means we're not actually testing against what
  our customers see. One gap is closed here in order to get newly added
  test working as expected but there's a larger issue that needs to be
  addressed at a later date.

* We are using a pattern of "try to create and retrieve on failure"
  rather than "try to get and create on failure" when determining
  whether to create availability zone records. This works where each
  operation results in a new transaction but does not when the
  transaction is shared. We switch this.

With these two issues addressed, the rest of the migration is relatively
straightforward.

Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Change-Id: Icfe4b225e216dfda46366af1eb3820b193befa0f
This commit is contained in:
Stephen Finucane 2022-09-06 11:56:28 +01:00
parent e1057ae66f
commit 83a7fdf010
4 changed files with 136 additions and 72 deletions

View File

@ -500,21 +500,22 @@ def share_resources_host_update(context, current_host, new_host):
@require_admin_context
@context_manager.writer
def service_destroy(context, service_id):
session = get_session()
with session.begin():
service_ref = service_get(context, service_id, session=session)
service_ref.soft_delete(session)
service_ref = _service_get(context, service_id)
service_ref.soft_delete(context.session)
@require_admin_context
def service_get(context, service_id, session=None):
result = (model_query(
context,
models.Service,
session=session).
filter_by(id=service_id).
first())
def _service_get(context, service_id):
result = (
model_query(
context,
models.Service,
).filter_by(
id=service_id,
).first()
)
if not result:
raise exception.ServiceNotFound(service_id=service_id)
@ -522,6 +523,13 @@ def service_get(context, service_id, session=None):
@require_admin_context
@context_manager.reader
def service_get(context, service_id):
return _service_get(context, service_id)
@require_admin_context
@context_manager.reader
def service_get_all(context, disabled=None):
query = model_query(context, models.Service)
@ -532,6 +540,7 @@ def service_get_all(context, disabled=None):
@require_admin_context
@context_manager.reader
def service_get_all_by_topic(context, topic):
return (model_query(
context, models.Service, read_deleted="no").
@ -541,6 +550,7 @@ def service_get_all_by_topic(context, topic):
@require_admin_context
@context_manager.reader
def service_get_by_host_and_topic(context, host, topic):
result = (model_query(
context, models.Service, read_deleted="no").
@ -554,39 +564,53 @@ def service_get_by_host_and_topic(context, host, topic):
@require_admin_context
def _service_get_all_topic_subquery(context, session, topic, subq, label):
def _service_get_all_topic_subquery(context, topic, subq, label):
sort_value = getattr(subq.c, label)
return (model_query(context, models.Service,
func.coalesce(sort_value, 0),
session=session, read_deleted="no").
filter_by(topic=topic).
filter_by(disabled=False).
outerjoin((subq, models.Service.host == subq.c.host)).
order_by(sort_value).
all())
return (
model_query(
context, models.Service,
func.coalesce(sort_value, 0),
read_deleted="no",
).filter_by(
topic=topic,
).filter_by(
disabled=False,
).outerjoin(
(subq, models.Service.host == subq.c.host)
).order_by(
sort_value
).all()
)
@require_admin_context
@context_manager.reader
def service_get_all_share_sorted(context):
session = get_session()
with session.begin():
topic = CONF.share_topic
label = 'share_gigabytes'
subq = (model_query(context, models.Share,
func.sum(models.Share.size).label(label),
session=session, read_deleted="no").
join(models.ShareInstance,
models.ShareInstance.share_id == models.Share.id).
group_by(models.ShareInstance.host).
subquery())
return _service_get_all_topic_subquery(context,
session,
topic,
subq,
label)
topic = CONF.share_topic
label = 'share_gigabytes'
subq = (
model_query(
context,
models.Share,
func.sum(models.Share.size).label(label),
read_deleted="no",
).join(
models.ShareInstance,
models.ShareInstance.share_id == models.Share.id,
).group_by(
models.ShareInstance.host
).subquery()
)
return _service_get_all_topic_subquery(
context,
topic,
subq,
label,
)
@require_admin_context
@context_manager.reader
def service_get_by_args(context, host, binary):
result = (model_query(context, models.Service).
filter_by(host=host).
@ -600,32 +624,28 @@ def service_get_by_args(context, host, binary):
@require_admin_context
@context_manager.writer
def service_create(context, values):
session = get_session()
_ensure_availability_zone_exists(context, values, session)
_ensure_availability_zone_exists(context, values)
service_ref = models.Service()
service_ref.update(values)
if not CONF.enable_new_services:
service_ref.disabled = True
with session.begin():
service_ref.save(session)
return service_ref
service_ref.save(context.session)
return service_ref
@require_admin_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
@context_manager.writer
def service_update(context, service_id, values):
session = get_session()
_ensure_availability_zone_exists(context, values, strict=False)
_ensure_availability_zone_exists(context, values, session, strict=False)
with session.begin():
service_ref = service_get(context, service_id, session=session)
service_ref.update(values)
service_ref.save(session=session)
service_ref = _service_get(context, service_id)
service_ref.update(values)
service_ref.save(context.session)
###################
@ -5307,7 +5327,9 @@ def share_type_extra_specs_update_or_create(context, share_type_id, specs):
return specs
def _ensure_availability_zone_exists(context, values, session, strict=True):
def _ensure_availability_zone_exists(
context, values, session=None, *, strict=True,
):
az_name = values.pop('availability_zone', None)
if strict and not az_name:
@ -5317,9 +5339,9 @@ def _ensure_availability_zone_exists(context, values, session, strict=True):
return
if uuidutils.is_uuid_like(az_name):
az_ref = availability_zone_get(context, az_name, session=session)
az_ref = _availability_zone_get(context, az_name, session=session)
else:
az_ref = availability_zone_create_if_not_exist(
az_ref = _availability_zone_create_if_not_exist(
context, az_name, session=session)
values.update({'availability_zone_id': az_ref['id']})
@ -5330,6 +5352,11 @@ def availability_zone_get(context, id_or_name, session=None):
if session is None:
session = get_session()
return _availability_zone_get(context, id_or_name, session=session)
@require_context
def _availability_zone_get(context, id_or_name, session=None):
query = model_query(context, models.AvailabilityZone, session=session)
if uuidutils.is_uuid_like(id_or_name):
@ -5345,21 +5372,23 @@ def availability_zone_get(context, id_or_name, session=None):
return result
# TODO(stephenfin): Remove the 'session' argument once all callers have been
# converted
@require_context
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
def availability_zone_create_if_not_exist(context, name, session=None):
if session is None:
session = get_session()
az = models.AvailabilityZone()
az.update({'id': uuidutils.generate_uuid(), 'name': name})
def _availability_zone_create_if_not_exist(context, name, session=None):
try:
with session.begin():
az.save(session)
# NOTE(u_glide): Do not catch specific exception here, because it depends
# on concrete backend used by SqlAlchemy
except Exception:
return availability_zone_get(context, name, session=session)
return _availability_zone_get(context, name, session=session)
except exception.AvailabilityZoneNotFound:
az = models.AvailabilityZone()
az.update({'id': uuidutils.generate_uuid(), 'name': name})
# TODO(stephenfin): Remove this branch once all callers have been
# updated not to pass 'session'
if session is not None:
with session.begin():
az.save(session)
else:
az.save(context.session)
return az

View File

@ -1202,6 +1202,9 @@ class DriverPrivateData(BASE, ManilaBase):
class AvailabilityZone(BASE, ManilaBase):
"""Represents a private data as key-value pairs for a driver."""
__tablename__ = 'availability_zones'
__table_args__ = (
schema.UniqueConstraint('name', 'deleted', name='az_name_uc'),
)
id = Column(String(36), primary_key=True, nullable=False)
deleted = Column(String(36), default='False')
name = Column(String(255), nullable=False)

View File

@ -72,6 +72,14 @@ class DatabaseFixture(fixtures.Fixture):
self.engine = db_session.get_engine()
self.engine.dispose()
conn = self.engine.connect()
# FIXME(stephenfin): This is an issue. We're not applying our
# migrations on SQLite in-memory backends and because the model schemas
# and migration schemas don't currently match exactly, we are not
# testing against something resembling what our customers would see.
# We should (a) start applying the migrations for all backends (which
# will require reworking the migrations since SQLite doesn't support
# ALTER fully, meaning batch mode must be used) and (b) get the two
# different sets of schemas in sync and keep them in sync.
if sql_connection == "sqlite://":
self.setup_sqlite(db_migrate)
else:

View File

@ -3101,8 +3101,10 @@ class ShareNetworkSubnetDatabaseAPITestCase(BaseDatabaseAPITestCase):
self.assertEqual(1, len(subnets))
def test_get_by_availability_zone_id(self):
az = db_api.availability_zone_create_if_not_exist(self.fake_context,
'fake_zone_id')
with db_api.context_manager.writer.using(self.fake_context):
az = db_api._availability_zone_create_if_not_exist(
self.fake_context, 'fake_zone_id',
)
self.subnet_dict['availability_zone_id'] = az['id']
db_api.share_network_subnet_create(self.fake_context, self.subnet_dict)
@ -3112,8 +3114,10 @@ class ShareNetworkSubnetDatabaseAPITestCase(BaseDatabaseAPITestCase):
self._check_fields(expected=self.subnet_dict, actual=result[0])
def test_get_az_subnets(self):
az = db_api.availability_zone_create_if_not_exist(self.fake_context,
'fake_zone_id')
with db_api.context_manager.writer.using(self.fake_context):
az = db_api._availability_zone_create_if_not_exist(
self.fake_context, 'fake_zone_id',
)
self.subnet_dict['availability_zone_id'] = az['id']
db_api.share_network_subnet_create(self.fake_context, self.subnet_dict)
@ -3710,6 +3714,21 @@ class ServiceDatabaseAPITestCase(test.TestCase):
self.assertEqual(az.id, service.availability_zone_id)
self.assertSubDictMatch(self.service_data, service.to_dict())
def test_create__az_exists(self):
# there's no public AZ create method so we have to define one ourselves
@db_api.context_manager.writer
def availability_zone_create(context, name):
return db_api._availability_zone_create_if_not_exist(
context, name,
)
az = availability_zone_create(self.ctxt, 'fake_zone')
service = db_api.service_create(self.ctxt, self.service_data)
self.assertEqual(az.id, service.availability_zone_id)
self.assertSubDictMatch(self.service_data, service.to_dict())
def test_update(self):
az_name = 'fake_zone2'
update_data = {"availability_zone": az_name}
@ -3744,7 +3763,10 @@ class AvailabilityZonesDatabaseAPITestCase(test.TestCase):
def test_az_get(self):
az_name = 'test_az'
az = db_api.availability_zone_create_if_not_exist(self.ctxt, az_name)
with db_api.context_manager.writer.using(self.ctxt):
az = db_api._availability_zone_create_if_not_exist(
self.ctxt, az_name
)
az_by_id = db_api.availability_zone_get(self.ctxt, az['id'])
az_by_name = db_api.availability_zone_get(self.ctxt, az_name)
@ -3755,9 +3777,11 @@ class AvailabilityZonesDatabaseAPITestCase(test.TestCase):
self.assertEqual(az['id'], az_by_name['id'])
def test_az_get_all(self):
db_api.availability_zone_create_if_not_exist(self.ctxt, 'test1')
db_api.availability_zone_create_if_not_exist(self.ctxt, 'test2')
db_api.availability_zone_create_if_not_exist(self.ctxt, 'test3')
with db_api.context_manager.writer.using(self.ctxt):
db_api._availability_zone_create_if_not_exist(self.ctxt, 'test1')
db_api._availability_zone_create_if_not_exist(self.ctxt, 'test2')
db_api._availability_zone_create_if_not_exist(self.ctxt, 'test3')
db_api.service_create(self.ctxt, {'availability_zone': 'test2'})
actual_result = db_api.availability_zone_get_all(self.ctxt)