mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-05 01:11:55 +02:00
WIP domain caching layer
This commit is contained in:
parent
d51a8600d4
commit
63d8198075
2 changed files with 270 additions and 13 deletions
|
@ -1,12 +1,16 @@
|
|||
import logging
|
||||
|
||||
from datetime import date
|
||||
from string import digits
|
||||
|
||||
from django.db import models
|
||||
|
||||
from epplibwrapper import (
|
||||
CLIENT as registry,
|
||||
commands,
|
||||
common as epp,
|
||||
RegistryError,
|
||||
ErrorCode,
|
||||
)
|
||||
|
||||
from .utility.domain_field import DomainField
|
||||
|
@ -40,6 +44,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
domain meets the required checks.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._cache = {}
|
||||
super(Domain, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class Status(models.TextChoices):
|
||||
"""
|
||||
The status codes we can receive from the registry.
|
||||
|
@ -93,6 +102,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
PENDING_TRANSFER = "pendingTransfer"
|
||||
PENDING_UPDATE = "pendingUpdate"
|
||||
|
||||
|
||||
class State(models.TextChoices):
|
||||
"""These capture (some of) the states a domain object can be in."""
|
||||
|
||||
|
@ -105,20 +115,54 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
# the state is indeterminate
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class Cache(property):
|
||||
"""
|
||||
Python descriptor to turn class methods into properties.
|
||||
|
||||
The purpose of subclassing `property` rather than using it directly
|
||||
as a decorator (`@Cache`) is to insert generic code to run
|
||||
before or after _all_ properties are accessed, modified, or deleted.
|
||||
|
||||
As an example:
|
||||
|
||||
domain = Domain(name="example.gov")
|
||||
domain.save()
|
||||
<--- insert code here
|
||||
date = domain.creation_date
|
||||
<--- or here
|
||||
(...other stuff...)
|
||||
"""
|
||||
|
||||
def __get__(self, obj, objtype=None):
|
||||
"""Called during get. Example: `r = domain.registrant`."""
|
||||
return super().__get__(obj, objtype)
|
||||
|
||||
def __set__(self, obj, value):
|
||||
"""Called during set. Example: `domain.registrant = 'abc123'`."""
|
||||
super().__set__(obj, value)
|
||||
# always invalidate cache after sending updates to the registry
|
||||
obj._invalidate_cache()
|
||||
|
||||
def __delete__(self, obj):
|
||||
"""Called during delete. Example: `del domain.registrant`."""
|
||||
super().__delete__(obj)
|
||||
|
||||
|
||||
@classmethod
|
||||
def available(cls, domain: str) -> bool:
|
||||
"""Check if a domain is available."""
|
||||
if not cls.string_could_be_domain(domain):
|
||||
raise ValueError("Not a valid domain: %s" % str(domain))
|
||||
req = commands.CheckDomain([domain])
|
||||
return registry.send(req).res_data[0].avail
|
||||
return registry.send(req, cleaned=True).res_data[0].avail
|
||||
|
||||
@classmethod
|
||||
def registered(cls, domain: str) -> bool:
|
||||
"""Check if a domain is _not_ available."""
|
||||
return not cls.available(domain)
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def contacts(self) -> dict[str, str]:
|
||||
"""
|
||||
Get a dictionary of registry IDs for the contacts for this domain.
|
||||
|
@ -129,22 +173,22 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def creation_date(self) -> date:
|
||||
"""Get the `cr_date` element from the registry."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def last_transferred_date(self) -> date:
|
||||
"""Get the `tr_date` element from the registry."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def last_updated_date(self) -> date:
|
||||
"""Get the `up_date` element from the registry."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def expiration_date(self) -> date:
|
||||
"""Get or set the `ex_date` element from the registry."""
|
||||
raise NotImplementedError()
|
||||
|
@ -153,12 +197,12 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
def expiration_date(self, ex_date: date):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def password(self) -> str:
|
||||
"""Get the `auth_info.pw` element from the registry. Not a real password."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def nameservers(self) -> list[tuple[str]]:
|
||||
"""
|
||||
Get or set a complete list of nameservers for this domain.
|
||||
|
@ -182,7 +226,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
# TODO: call EPP to set this info.
|
||||
pass
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def statuses(self) -> list[str]:
|
||||
"""
|
||||
Get or set the domain `status` elements from the registry.
|
||||
|
@ -200,7 +244,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
# some statuses cannot be set by the client at all
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def registrant_contact(self) -> PublicContact:
|
||||
"""Get or set the registrant for this domain."""
|
||||
raise NotImplementedError()
|
||||
|
@ -209,7 +253,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
def registrant_contact(self, contact: PublicContact):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def administrative_contact(self) -> PublicContact:
|
||||
"""Get or set the admin contact for this domain."""
|
||||
raise NotImplementedError()
|
||||
|
@ -218,7 +262,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
def administrative_contact(self, contact: PublicContact):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def security_contact(self) -> PublicContact:
|
||||
"""Get or set the security contact for this domain."""
|
||||
# TODO: replace this with a real implementation
|
||||
|
@ -232,7 +276,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
# TODO: replace this with a real implementation
|
||||
pass
|
||||
|
||||
@property
|
||||
@Cache
|
||||
def technical_contact(self) -> PublicContact:
|
||||
"""Get or set the tech contact for this domain."""
|
||||
raise NotImplementedError()
|
||||
|
@ -290,3 +334,215 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
|
||||
# ForeignKey on DomainInvitation creates an "invitations" member for
|
||||
# all of the invitations that have been sent for this domain
|
||||
|
||||
def _validate_host_tuples(self, hosts: list[tuple[str]]):
|
||||
"""
|
||||
Helper function. Validate hostnames and IP addresses.
|
||||
|
||||
Raises:
|
||||
ValueError if hostname or IP address appears invalid or mismatched.
|
||||
"""
|
||||
for host in hosts:
|
||||
hostname = host[0].lower()
|
||||
addresses: tuple[str] = host[1:] # type: ignore
|
||||
if not bool(Domain.HOST_REGEX.match(hostname)):
|
||||
raise ValueError("Invalid hostname: %s." % hostname)
|
||||
if len(hostname) > Domain.MAX_LENGTH:
|
||||
raise ValueError("Too long hostname: %s" % hostname)
|
||||
|
||||
is_subordinate = hostname.split(".", 1)[-1] == self.name
|
||||
if is_subordinate and len(addresses) == 0:
|
||||
raise ValueError("Must supply IP addresses for %s" % hostname)
|
||||
if not is_subordinate and len(addresses) > 0:
|
||||
raise ValueError("Must not supply IP addresses for %s" % hostname)
|
||||
|
||||
for address in addresses:
|
||||
allow = set(":." + digits)
|
||||
if any(c not in allow for c in address):
|
||||
raise ValueError("Invalid IP address: %s." % address)
|
||||
|
||||
def _get_or_create_domain(self):
|
||||
"""Try to fetch info about this domain. Create it if it does not exist."""
|
||||
already_tried_to_create = False
|
||||
while True:
|
||||
try:
|
||||
req = commands.InfoDomain(name=self.name)
|
||||
return registry.send(req).res_data[0]
|
||||
except RegistryError as e:
|
||||
if already_tried_to_create:
|
||||
raise e
|
||||
if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST:
|
||||
# avoid infinite loop
|
||||
already_tried_to_create = True
|
||||
registrant = self._get_or_create_contact(
|
||||
PublicContact.get_default_registrant()
|
||||
)
|
||||
req = commands.CreateDomain(
|
||||
name=self.name,
|
||||
registrant=registrant.id,
|
||||
auth_info=epp.DomainAuthInfo(
|
||||
pw="2fooBAR123fooBaz"
|
||||
), # not a password
|
||||
)
|
||||
registry.send(req)
|
||||
# no error, so go ahead and update state
|
||||
self.state = Domain.State.CREATED
|
||||
self.save()
|
||||
else:
|
||||
raise e
|
||||
|
||||
def _get_or_create_contact(self, contact: PublicContact):
|
||||
"""Try to fetch info about a contact. Create it if it does not exist."""
|
||||
while True:
|
||||
try:
|
||||
req = commands.InfoContact(id=contact.registry_id)
|
||||
return registry.send(req).res_data[0]
|
||||
except RegistryError as e:
|
||||
if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST:
|
||||
create = commands.CreateContact(
|
||||
id=contact.registry_id,
|
||||
postal_info=epp.PostalInfo( # type: ignore
|
||||
name=contact.name,
|
||||
addr=epp.ContactAddr(
|
||||
street=[
|
||||
getattr(contact, street)
|
||||
for street in ["street1", "street2", "street3"]
|
||||
if hasattr(contact, street)
|
||||
],
|
||||
city=contact.city,
|
||||
pc=contact.pc,
|
||||
cc=contact.cc,
|
||||
sp=contact.sp,
|
||||
),
|
||||
org=contact.org,
|
||||
type="loc",
|
||||
),
|
||||
email=contact.email,
|
||||
voice=contact.voice,
|
||||
fax=contact.fax,
|
||||
auth_info=epp.ContactAuthInfo(pw="2fooBAR123fooBaz"),
|
||||
)
|
||||
# security contacts should only show email addresses, for now
|
||||
if (
|
||||
contact.contact_type
|
||||
== PublicContact.ContactTypeChoices.SECURITY
|
||||
):
|
||||
DF = epp.DiscloseField
|
||||
create.disclose = epp.Disclose(
|
||||
flag=False,
|
||||
fields={DF.FAX, DF.VOICE, DF.ADDR},
|
||||
types={DF.ADDR: "loc"},
|
||||
)
|
||||
registry.send(create)
|
||||
else:
|
||||
raise e
|
||||
|
||||
def _update_or_create_host(self, host):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _delete_host(self, host):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
|
||||
"""Contact registry for info about a domain."""
|
||||
try:
|
||||
# get info from registry
|
||||
data = self._get_or_create_domain()
|
||||
# extract properties from response
|
||||
# (Ellipsis is used to mean "null")
|
||||
cache = {
|
||||
"auth_info": getattr(data, "auth_info", ...),
|
||||
"_contacts": getattr(data, "contacts", ...),
|
||||
"cr_date": getattr(data, "cr_date", ...),
|
||||
"ex_date": getattr(data, "ex_date", ...),
|
||||
"_hosts": getattr(data, "hosts", ...),
|
||||
"name": getattr(data, "name", ...),
|
||||
"registrant": getattr(data, "registrant", ...),
|
||||
"statuses": getattr(data, "statuses", ...),
|
||||
"tr_date": getattr(data, "tr_date", ...),
|
||||
"up_date": getattr(data, "up_date", ...),
|
||||
}
|
||||
# remove null properties (to distinguish between "a value of None" and null)
|
||||
cleaned = {k: v for k, v in cache if v is not ...}
|
||||
|
||||
# get contact info, if there are any
|
||||
if (
|
||||
fetch_contacts
|
||||
and "_contacts" in cleaned
|
||||
and isinstance(cleaned["_contacts"], list)
|
||||
and len(cleaned["_contacts"])
|
||||
):
|
||||
cleaned["contacts"] = []
|
||||
for id in cleaned["_contacts"]:
|
||||
# we do not use _get_or_create_* because we expect the object we
|
||||
# just asked the registry for still exists --
|
||||
# if not, that's a problem
|
||||
req = commands.InfoContact(id=id)
|
||||
data = registry.send(req).res_data[0]
|
||||
# extract properties from response
|
||||
# (Ellipsis is used to mean "null")
|
||||
contact = {
|
||||
"id": id,
|
||||
"auth_info": getattr(data, "auth_info", ...),
|
||||
"cr_date": getattr(data, "cr_date", ...),
|
||||
"disclose": getattr(data, "disclose", ...),
|
||||
"email": getattr(data, "email", ...),
|
||||
"fax": getattr(data, "fax", ...),
|
||||
"postal_info": getattr(data, "postal_info", ...),
|
||||
"statuses": getattr(data, "statuses", ...),
|
||||
"tr_date": getattr(data, "tr_date", ...),
|
||||
"up_date": getattr(data, "up_date", ...),
|
||||
"voice": getattr(data, "voice", ...),
|
||||
}
|
||||
cleaned["contacts"].append(
|
||||
{k: v for k, v in contact if v is not ...}
|
||||
)
|
||||
|
||||
# get nameserver info, if there are any
|
||||
if (
|
||||
fetch_hosts
|
||||
and "_hosts" in cleaned
|
||||
and isinstance(cleaned["_hosts"], list)
|
||||
and len(cleaned["_hosts"])
|
||||
):
|
||||
cleaned["hosts"] = []
|
||||
for name in cleaned["_hosts"]:
|
||||
# we do not use _get_or_create_* because we expect the object we
|
||||
# just asked the registry for still exists --
|
||||
# if not, that's a problem
|
||||
req = commands.InfoHost(name=name)
|
||||
data = registry.send(req).res_data[0]
|
||||
# extract properties from response
|
||||
# (Ellipsis is used to mean "null")
|
||||
host = {
|
||||
"name": name,
|
||||
"addrs": getattr(data, "addrs", ...),
|
||||
"cr_date": getattr(data, "cr_date", ...),
|
||||
"statuses": getattr(data, "statuses", ...),
|
||||
"tr_date": getattr(data, "tr_date", ...),
|
||||
"up_date": getattr(data, "up_date", ...),
|
||||
}
|
||||
cleaned["hosts"].append({k: v for k, v in host if v is not ...})
|
||||
|
||||
# replace the prior cache with new data
|
||||
self._cache = cleaned
|
||||
except RegistryError as e:
|
||||
logger.error(e)
|
||||
|
||||
def _invalidate_cache(self):
|
||||
"""Remove cache data when updates are made."""
|
||||
self._cache = {}
|
||||
|
||||
def _get_property(self, property):
|
||||
"""Get some piece of info about a domain."""
|
||||
if property not in self._cache:
|
||||
self._fetch_cache(
|
||||
fetch_hosts=(property == "hosts"),
|
||||
fetch_contacts=(property == "contacts"),
|
||||
)
|
||||
if property in self._cache:
|
||||
return self._cache[property]
|
||||
else:
|
||||
raise KeyError(
|
||||
"Requested key %s was not found in registry cache." % str(property)
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue