83 lines
2.8 KiB
Python
83 lines
2.8 KiB
Python
|
|
"""ZeroTier membership model — observed controller-side member state."""
|
||
|
|
|
||
|
|
from gatehouse_app.extensions import db
|
||
|
|
from gatehouse_app.models.base import BaseModel
|
||
|
|
|
||
|
|
|
||
|
|
class ZeroTierMembership(BaseModel):
|
||
|
|
"""Observed state of a node in a ZeroTier network from the controller API.
|
||
|
|
|
||
|
|
This is maintained as a cache of controller-side state, updated by the
|
||
|
|
reconciliation loop and by direct API calls. The raw_controller_payload
|
||
|
|
column stores the full API response for debugging and audit.
|
||
|
|
|
||
|
|
Keyed by zerotier_network_id + node_id (unique constraint).
|
||
|
|
|
||
|
|
Attributes:
|
||
|
|
organization_id: FK to the organization
|
||
|
|
device_network_membership_id: FK to the portal's membership record (nullable)
|
||
|
|
zerotier_network_id: The 16-char hex ZeroTier network ID
|
||
|
|
node_id: The 10-char hex ZeroTier node ID
|
||
|
|
member_seen: Whether the controller has ever seen this member
|
||
|
|
authorized: Current authorization state from ZeroTier
|
||
|
|
join_seen_at: When the member was first seen joining
|
||
|
|
last_synced_at: When we last polled ZeroTier for this member
|
||
|
|
raw_controller_payload: Full API response for debugging
|
||
|
|
"""
|
||
|
|
|
||
|
|
__tablename__ = "zerotier_memberships"
|
||
|
|
|
||
|
|
organization_id = db.Column(
|
||
|
|
db.String(36),
|
||
|
|
db.ForeignKey("organizations.id"),
|
||
|
|
nullable=False,
|
||
|
|
index=True,
|
||
|
|
)
|
||
|
|
device_network_membership_id = db.Column(
|
||
|
|
db.String(36),
|
||
|
|
db.ForeignKey("device_network_memberships.id"),
|
||
|
|
nullable=True,
|
||
|
|
index=True,
|
||
|
|
)
|
||
|
|
zerotier_network_id = db.Column(
|
||
|
|
db.String(16),
|
||
|
|
nullable=False,
|
||
|
|
index=True,
|
||
|
|
)
|
||
|
|
node_id = db.Column(
|
||
|
|
db.String(10),
|
||
|
|
nullable=False,
|
||
|
|
index=True,
|
||
|
|
)
|
||
|
|
member_seen = db.Column(db.Boolean, default=False, nullable=False)
|
||
|
|
authorized = db.Column(db.Boolean, default=False, nullable=False)
|
||
|
|
join_seen_at = db.Column(db.DateTime(timezone=True), nullable=True)
|
||
|
|
last_synced_at = db.Column(db.DateTime(timezone=True), nullable=True)
|
||
|
|
raw_controller_payload = db.Column(db.JSON, nullable=True)
|
||
|
|
|
||
|
|
# Relationships
|
||
|
|
organization = db.relationship("Organization", backref="zerotier_memberships")
|
||
|
|
device_network_membership = db.relationship(
|
||
|
|
"DeviceNetworkMembership",
|
||
|
|
back_populates="zerotier_membership",
|
||
|
|
)
|
||
|
|
|
||
|
|
__table_args__ = (
|
||
|
|
db.UniqueConstraint(
|
||
|
|
"zerotier_network_id",
|
||
|
|
"node_id",
|
||
|
|
name="uix_zt_network_node",
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
def __repr__(self):
|
||
|
|
return (
|
||
|
|
f"<ZeroTierMembership network={self.zerotier_network_id} "
|
||
|
|
f"node={self.node_id} authorized={self.authorized}>"
|
||
|
|
)
|
||
|
|
|
||
|
|
def to_dict(self, exclude=None):
|
||
|
|
exclude = exclude or []
|
||
|
|
data = super().to_dict(exclude=exclude)
|
||
|
|
return data
|