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.

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

Change-Id: I4c218793948b0ed7ce0c143d6c794516fdeb224f
This commit is contained in:
Tom Weininger
2022-04-06 15:00:54 +02:00
parent 309c7e61fe
commit ff6d0c5280
3 changed files with 155 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))