Improve string representation of DB models

This replaces the default "<XYClass object at 0xdeadbeeff00>"
repr() for DB objects, which is pretty useless.
This change makes it easier to identify DB objects in debuggers
(and logs, if objects get logged).

__repr__() and __str__() have slightly different purposes.

The purpose of __repr__() is to return a string that uniquely identifies
a single object. __str__(), on the other hand, should produce a human
readable string. Another thing that is worth to note is that str() will
call __repr__() if __str__() is not implemented, repr() will always call
__repr__(). More details can be found under [1].

By implementing a generic __repr__() in the base class
there should be no need to override it in the models classes.
However, one might not be interested in seeing all
the object's attributes in a string representation
(which is what the generic __repr__() would do) or
certain sensitive information should be hidden from users.
In those cases it makes sense to implement __str__() for certain
classes specifically.

Release notes were taken from a follow-up change [2].

[1]: https://stackoverflow.com/a/2626364/6265131
[2]: I9b9eb3158b099cf04d788692fc9e5efcdfebf93e

Change-Id: I4c218793948b0ed7ce0c143d6c794516fdeb224f
(cherry picked from commit ff6d0c5280)
This commit is contained in:
Tom Weininger 2022-04-06 15:00:54 +02:00
parent a237182303
commit 2cb52fe60a
4 changed files with 162 additions and 8 deletions

View File

@ -132,6 +132,13 @@ class OctaviaBase(models.ModelBase):
query = query.join(getattr(model, k)).filter_by(**v)
return query
def __repr__(self):
params = sorted(
(k, getattr(self, k)) for k in self.__mapper__.columns.keys()
)
params = ", ".join(f"{k}={v!r}" for k, v in params)
return f"{self.__class__.__name__}({params})"
class LookupTableMixin(object):
"""Mixin to add to classes that are lookup tables."""

View File

@ -237,6 +237,15 @@ class Member(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin,
primaryjoin='and_(foreign(Tags.resource_id)==Member.id)'
)
def __str__(self):
return (f"Member(id={self.id!r}, name={self.name!r}, "
f"project_id={self.project_id!r}, "
f"provisioning_status={self.provisioning_status!r}, "
f"ip_address={self.ip_address!r}, "
f"protocol_port={self.protocol_port!r}, "
f"operating_status={self.operating_status!r}, "
f"weight={self.weight!r})")
class HealthMonitor(base_models.BASE, base_models.IdMixin,
base_models.ProjectMixin, models.TimestampMixin,
@ -293,6 +302,11 @@ class HealthMonitor(base_models.BASE, base_models.IdMixin,
http_version = sa.Column(sa.Float, nullable=True)
domain_name = sa.Column(sa.String(255), nullable=True)
def __str__(self):
return (f"HealthMonitor(id={self.id!r}, name={self.name!r}, "
f"project_id={self.project_id!r}, type={self.type!r}, "
f"enabled={self.enabled!r})")
class Pool(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin,
models.TimestampMixin, base_models.NameMixin, base_models.TagMixin):
@ -372,6 +386,14 @@ class Pool(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin,
_l_ids.append(li.id)
return _listeners
def __str__(self):
return (f"Pool(id={self.id!r}, name={self.name!r}, "
f"project_id={self.project_id!r}, "
f"provisioning_status={self.provisioning_status!r}, "
f"protocol={self.protocol!r}, "
f"lb_algorithm={self.lb_algorithm!r}, "
f"enabled={self.enabled!r})")
class LoadBalancer(base_models.BASE, base_models.IdMixin,
base_models.ProjectMixin, models.TimestampMixin,
@ -425,6 +447,13 @@ class LoadBalancer(base_models.BASE, base_models.IdMixin,
name="fk_load_balancer_availability_zone_name"),
nullable=True)
def __str__(self):
return (f"LoadBalancer(id={self.id!r}, name={self.name!r}, "
f"project_id={self.project_id!r}, vip={self.vip!r}, "
f"provisioning_status={self.provisioning_status!r}, "
f"operating_status={self.operating_status!r}, "
f"provider={self.provider!r})")
class VRRPGroup(base_models.BASE):
@ -574,6 +603,13 @@ class Listener(base_models.BASE, base_models.IdMixin,
'ListenerCidr', cascade='all,delete-orphan',
uselist=True, backref=orm.backref('listener', uselist=False))
def __str__(self):
return (f"Listener(id={self.id!r}, "
f"default_pool={self.default_pool!r}, name={self.name!r}, "
f"project_id={self.project_id!r}, protocol={self.protocol!r}, "
f"protocol_port={self.protocol_port!r}, "
f"enabled={self.enabled!r})")
class SNI(base_models.BASE):
@ -630,6 +666,12 @@ class Amphora(base_models.BASE, base_models.IdMixin, models.TimestampMixin):
back_populates='amphorae')
compute_flavor = sa.Column(sa.String(255), nullable=True)
def __str__(self):
return (f"Amphora(id={self.id!r}, load_balancer_id="
f"{self.load_balancer_id!r}, status={self.status!r}, "
f"role={self.role!r}, lb_network_ip={self.lb_network_ip!r}, "
f"vrrp_ip={self.vrrp_ip!r})")
class AmphoraHealth(base_models.BASE):
__data_model__ = data_models.AmphoraHealth
@ -692,6 +734,12 @@ class L7Rule(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin,
primaryjoin='and_(foreign(Tags.resource_id)==L7Rule.id)'
)
def __str__(self):
return (f"L7Rule(id={self.id!r}, project_id={self.project_id!r}, "
f"provisioning_status={self.provisioning_status!r}, "
f"type={self.type!r}, key={self.key!r}, value={self.value!r}, "
f"invert={self.invert!r}, enabled={self.enabled!r})")
class L7Policy(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin,
models.TimestampMixin, base_models.NameMixin,
@ -751,6 +799,13 @@ class L7Policy(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin,
primaryjoin='and_(foreign(Tags.resource_id)==L7Policy.id)'
)
def __str__(self):
return (f"L7Policy(id={self.id!r}, name={self.name!r}, "
f"project_id={self.project_id!r}, "
f"provisioning_status={self.provisioning_status!r}, "
f"action={self.action!r}, position={self.position!r}, "
f"enabled={self.enabled!r})")
class Quotas(base_models.BASE):
@ -776,6 +831,14 @@ class Quotas(base_models.BASE):
in_use_l7policy = sa.Column(sa.Integer(), nullable=True)
in_use_l7rule = sa.Column(sa.Integer(), nullable=True)
def __str__(self):
return (f"Quotas(project_id={self.project_id!r}, "
f"load_balancer={self.load_balancer!r}, "
f"listener={self.listener!r}, pool={self.pool!r}, "
f"health_monitor={self.health_monitor!r}, "
f"member={self.member!r}, l7policy={self.l7policy!r}, "
f"l7rule={self.l7rule!r})")
class FlavorProfile(base_models.BASE, base_models.IdMixin,
base_models.NameMixin):

View File

@ -197,11 +197,21 @@ class ModelTestMixin(object):
kwargs = {'listener_id': listener_id, 'cidr': cidr}
return self._insert(session, models.ListenerCidr, kwargs)
def create_quotas(self, session, **overrides):
kwargs = {"project_id": self.FAKE_UUID_1}
kwargs.update(overrides)
return self._insert(session, models.Quotas, kwargs)
class PoolModelTest(base.OctaviaDBTestBase, ModelTestMixin):
def test_create(self):
pool = self.create_pool(self.session)
self.assertEqual(f"Pool(id={pool.id!r}, name=None, "
f"project_id={pool.project_id!r}, "
f"provisioning_status='ACTIVE', protocol='HTTP', "
f"lb_algorithm='LEAST_CONNECTIONS', enabled=True)",
str(pool))
self.assertIsNotNone(pool.created_at)
self.assertIsNone(pool.updated_at)
@ -276,6 +286,12 @@ class MemberModelTest(base.OctaviaDBTestBase, ModelTestMixin):
def test_create(self):
member = self.create_member(self.session, self.pool.id)
self.assertEqual(f"Member(id={member.id!r}, name=None, "
f"project_id={member.project_id!r}, "
f"provisioning_status='ACTIVE', "
f"ip_address='10.0.0.1', protocol_port=80, "
f"operating_status='ONLINE', weight=None)",
str(member))
self.assertIsNotNone(member.created_at)
self.assertIsNone(member.updated_at)
@ -321,7 +337,11 @@ class SessionPersistenceModelTest(base.OctaviaDBTestBase, ModelTestMixin):
self.pool = self.create_pool(self.session)
def test_create(self):
self.create_session_persistence(self.session, self.pool.id)
obj = self.create_session_persistence(self.session, self.pool.id)
self.assertEqual(f"SessionPersistence(cookie_name='cookie_name', "
f"persistence_granularity=None, "
f"persistence_timeout=None, pool_id={obj.pool_id!r}, "
f"type='HTTP_COOKIE')", str(obj))
def test_update(self):
session_persistence = self.create_session_persistence(self.session,
@ -353,6 +373,10 @@ class ListenerModelTest(base.OctaviaDBTestBase, ModelTestMixin):
def test_create(self):
listener = self.create_listener(self.session)
self.assertEqual(f"Listener(id={listener.id!r}, default_pool=None, "
f"name=None, project_id={listener.project_id!r}, "
f"protocol='HTTP', protocol_port=80, enabled=True)",
str(listener))
self.assertIsNotNone(listener.created_at)
self.assertIsNone(listener.updated_at)
@ -454,8 +478,12 @@ class ListenerStatisticsModelTest(base.OctaviaDBTestBase, ModelTestMixin):
self.amphora = self.create_amphora(self.session)
def test_create(self):
self.create_listener_statistics(self.session, self.listener.id,
self.amphora.id)
obj = self.create_listener_statistics(self.session, self.listener.id,
self.amphora.id)
self.assertEqual(f"ListenerStatistics(active_connections=0, "
f"amphora_id={obj.amphora_id!r}, bytes_in=0, "
f"bytes_out=0, listener_id={obj.listener_id!r}, "
f"request_errors=0, total_connections=0)", str(obj))
def test_create_with_negative_int(self):
overrides = {'bytes_in': -1}
@ -490,7 +518,10 @@ class HealthMonitorModelTest(base.OctaviaDBTestBase, ModelTestMixin):
self.pool = self.create_pool(self.session)
def test_create(self):
self.create_health_monitor(self.session, self.pool.id)
obj = self.create_health_monitor(self.session, self.pool.id)
self.assertEqual(f"HealthMonitor(id={obj.id!r}, name=None, "
f"project_id={obj.project_id!r}, type='HTTP', "
f"enabled=True)", str(obj))
def test_update(self):
health_monitor = self.create_health_monitor(self.session, self.pool.id)
@ -524,6 +555,11 @@ class LoadBalancerModelTest(base.OctaviaDBTestBase, ModelTestMixin):
def test_create(self):
load_balancer = self.create_load_balancer(self.session)
self.assertEqual(f"LoadBalancer(id={load_balancer.id!r}, name=None, "
f"project_id={load_balancer.project_id!r}, "
f"vip=None, provisioning_status='ACTIVE', "
f"operating_status='ONLINE', provider=None)",
str(load_balancer))
self.assertIsNotNone(load_balancer.created_at)
self.assertIsNone(load_balancer.updated_at)
@ -582,7 +618,11 @@ class VipModelTest(base.OctaviaDBTestBase, ModelTestMixin):
self.load_balancer = self.create_load_balancer(self.session)
def test_create(self):
self.create_vip(self.session, self.load_balancer.id)
obj = self.create_vip(self.session, self.load_balancer.id)
self.assertEqual(f"Vip(ip_address=None, "
f"load_balancer_id={obj.load_balancer_id!r}, "
f"network_id=None, octavia_owned=None, port_id=None, "
f"qos_policy_id=None, subnet_id=None)", str(obj))
def test_update(self):
vip = self.create_vip(self.session, self.load_balancer.id)
@ -616,7 +656,11 @@ class SNIModelTest(base.OctaviaDBTestBase, ModelTestMixin):
self.listener = self.create_listener(self.session)
def test_create(self):
self.create_sni(self.session, listener_id=self.listener.id)
obj = self.create_sni(self.session, listener_id=self.listener.id)
self.assertEqual(f"SNI(listener_id={obj.listener_id!r}, "
f"position=None, "
f"tls_container_id={obj.tls_container_id!r})",
str(obj))
def test_update(self):
sni = self.create_sni(self.session, listener_id=self.listener.id)
@ -649,7 +693,11 @@ class AmphoraModelTest(base.OctaviaDBTestBase, ModelTestMixin):
self.load_balancer = self.create_load_balancer(self.session)
def test_create(self):
self.create_amphora(self.session)
obj = self.create_amphora(self.session)
self.assertEqual(f"Amphora(id={obj.id!r}, load_balancer_id=None, "
f"status='ACTIVE', role=None, "
f"lb_network_ip='10.0.0.1', vrrp_ip='10.0.0.1')",
str(obj))
def test_update(self):
amphora = self.create_amphora(
@ -684,7 +732,10 @@ class AmphoraHealthModelTest(base.OctaviaDBTestBase, ModelTestMixin):
self.amphora = self.create_amphora(self.session)
def test_create(self):
self.create_amphora_health(self.session)
obj = self.create_amphora_health(self.session)
self.assertEqual(f"AmphoraHealth(amphora_id={obj.amphora_id!r}, "
f"busy=True, last_update={obj.last_update!r})",
str(obj))
def test_update(self):
amphora_health = self.create_amphora_health(self.session)
@ -715,6 +766,10 @@ class L7PolicyModelTest(base.OctaviaDBTestBase, ModelTestMixin):
def test_create(self):
l7policy = self.create_l7policy(self.session, self.listener.id)
self.assertEqual(f"L7Policy(id={l7policy.id!r}, name=None, "
f"project_id=None, provisioning_status='ACTIVE', "
f"action='REJECT', position=1, enabled=True)",
str(l7policy))
self.assertIsInstance(l7policy, models.L7Policy)
def test_update(self):
@ -836,6 +891,10 @@ class L7RuleModelTest(base.OctaviaDBTestBase, ModelTestMixin):
def test_create(self):
l7rule = self.create_l7rule(self.session, self.l7policy.id)
self.assertEqual(f"L7Rule(id={l7rule.id!r}, project_id=None, "
f"provisioning_status='ACTIVE', type='PATH', "
f"key=None, value='/api', invert=False, "
f"enabled=True)", str(l7rule))
self.assertIsInstance(l7rule, models.L7Rule)
def test_update(self):
@ -1757,6 +1816,9 @@ class FlavorModelTest(base.OctaviaDBTestBase, ModelTestMixin):
def test_create(self):
flavor = self.create_flavor(self.session, self.profile.id)
self.assertEqual(f"Flavor(description='fake flavor', enabled=True, "
f"flavor_profile_id={flavor.flavor_profile_id!r}, "
f"id={flavor.id!r}, name='fake_flavor')", str(flavor))
self.assertIsNotNone(flavor.id)
def test_delete(self):
@ -1776,6 +1838,9 @@ class FlavorProfileModelTest(base.OctaviaDBTestBase, ModelTestMixin):
def test_create(self):
fp = self.create_flavor_profile(self.session)
self.assertEqual(f"FlavorProfile(flavor_data={fp.flavor_data!r}, "
f"id={fp.id!r}, name='fake_profile', "
f"provider_name='fake_provider')", str(fp))
self.assertIsNotNone(fp.id)
def test_delete(self):
@ -1789,3 +1854,15 @@ class FlavorProfileModelTest(base.OctaviaDBTestBase, ModelTestMixin):
new_fp = self.session.query(
models.FlavorProfile).filter_by(id=id).first()
self.assertIsNone(new_fp)
class QuotasModelTest(base.OctaviaDBTestBase, ModelTestMixin):
def test_create(self):
obj = self.create_quotas(self.session, load_balancer=1, listener=2,
pool=3, health_monitor=4, member=5,
l7policy=6, l7rule=8)
self.assertEqual(f"Quotas(project_id={obj.project_id!r}, "
f"load_balancer=1, listener=2, pool=3, "
f"health_monitor=4, member=5, l7policy=6, l7rule=8)",
str(obj))

View File

@ -0,0 +1,7 @@
---
other:
- |
The string representation of data base model objects has been improved.
Calling str() on them will return a certain subset of fields and calling
repr() on them will return all fields. This is helpful for debugging, but
it may also change some of the log messages that Octavia emits.