def test_separate_fqdn_returns_atsign_for_top_of_domain(self): name = "%s.%s.%s" % ( factory.make_name("a"), factory.make_name("b"), factory.make_name("c")) factory.make_Domain(name=name) self.assertEqual(('@', name), separate_fqdn(name))
def create(self, request): """Create a dnsresourcerecord. :param fqdn: Hostname (with domain) for the dnsresource. Either fqdn or (name, domain) must be specified. Fqdn is ignored if either name or domain is given. :param name: Hostname (without domain) :param domain: Domain (name or id) :param rrtype: resource type to create :param rrdata: resource data (everything to the right of resource type.) """ data = request.data.copy() domain = None fqdn = data.get('fqdn', None) name = data.get('name', None) domainname = data.get('domain', None) rrtype = data.get('rrtype', None) rrdata = data.get('rrdata', None) if rrtype is None: raise MAASAPIBadRequest("rrtype must be provided.") if rrdata is None: raise MAASAPIBadRequest("rrdata must be provided.") # If the user gave us fqdn and did not give us name/domain, expand # fqdn. if domainname is None and name is None and fqdn is not None: # We need a type for resource separation. If the user didn't give # us a rrtype, then assume it's an address of some sort. rrtype = data.get('rrtype', None) (name, domainname) = separate_fqdn(fqdn, rrtype) data['domain'] = domainname data['name'] = name # If the domain is a name, make it an id. if domainname is not None: if domainname.isdigit(): domain = Domain.objects.get_domain_or_404( domainname, user=request.user, perm=NODE_PERMISSION.VIEW) else: domain = Domain.objects.get_domain_or_404( "name:%s" % domainname, user=request.user, perm=NODE_PERMISSION.VIEW) data['domain'] = domain.id if domain is None or name is None: raise MAASAPIValidationError( "Either name and domain (or fqdn) must be specified") # Do we already have a DNSResource for this fqdn? dnsrr = DNSResource.objects.filter(name=name, domain__id=domain.id) if not dnsrr.exists(): form = DNSResourceForm(data=data, request=request) if form.is_valid(): form.save() else: raise MAASAPIValidationError(form.errors) dnsrr = DNSResource.objects.filter(name=name, domain__id=domain.id) data['dnsresource'] = dnsrr form = DNSDataForm(data=data) if form.is_valid(): return form.save() else: raise MAASAPIValidationError(form.errors)
def read(self, request): """List all resources for the specified criteria. :param domain: restrict the listing to entries for the domain. :param name: restrict the listing to entries of the given name. :param rrtype: restrict the listing to entries which have records of the given rrtype. """ data = request.GET fqdn = data.get('fqdn', None) name = data.get('name', None) domainname = data.get('domain', None) rrtype = data.get('rrtype', None) if domainname is None and name is None and fqdn is not None: # We need a type for resource separation. If the user didn't give # us a rrtype, then assume it's an address of some sort. (name, domainname) = separate_fqdn(fqdn, rrtype) # If the domain is a name, make it an id. if domainname is not None: if domainname.isdigit(): domain = Domain.objects.get_domain_or_404( domainname, user=request.user, perm=NODE_PERMISSION.VIEW) else: domain = Domain.objects.get_domain_or_404( "name:%s" % domainname, user=request.user, perm=NODE_PERMISSION.VIEW) query = domain.dnsresource_set.all().order_by('name') else: query = DNSResource.objects.all().order_by('domain_id', 'name') if name is not None: query = query.filter(name=name) if rrtype is not None: query = query.filter(dnsdata__rrtype=rrtype) return query
def test_separate_fqdn_allows_domain_override(self): parent = "%s.%s" % (factory.make_name("b"), factory.make_name("c")) label = "%s.%s" % (factory.make_name("a"), factory.make_name("d")) name = "%s.%s" % (label, parent) factory.make_Domain(name=parent) self.assertEqual((label, parent), separate_fqdn(name, domainname=parent))
def read(self, request): """List all resources for the specified criteria. :param domain: restrict the listing to entries for the domain. :param name: restrict the listing to entries of the given name. :param rrtype: restrict the listing to entries which have records of the given rrtype. :param all: if True, also include implicit DNS records created for nodes registered in MAAS. """ data = request.GET fqdn = data.get('fqdn', None) name = data.get('name', None) domainname = data.get('domain', None) rrtype = data.get('rrtype', None) if rrtype is not None: rrtype = rrtype.upper() _all = get_optional_param(request.GET, 'all', default=False, validator=StringBool) if domainname is None and name is None and fqdn is not None: # We need a type for resource separation. If the user didn't give # us a rrtype, then assume it's an address of some sort. (name, domainname) = separate_fqdn(fqdn, rrtype) user = request.user return get_dnsresource_queryset(_all, domainname, name, rrtype, user)
def create(self, request): """@description-title Create a DNS resource @description Create a DNS resource. @param (string) "fqdn" [required=false] Hostname (with domain) for the dnsresource. Either ``fqdn`` or ``name`` and ``domain`` must be specified. ``fqdn`` is ignored if either ``name`` or ``domain`` is given. @param (string) "name" [required=true] Hostname (without domain). @param (string) "domain" [required=true] Domain (name or id). @param (string) "address_ttl" [required=false] Default TTL for entries in this zone. @param (string) "ip_addresses" [required=false] Address (ip or id) to assign to the dnsresource. This creates an A or AAAA record, for each of the supplied ip_addresses, IPv4 or IPv6, respectively. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing the new DNS resource object. @success-example "success-json" [exkey=dnsresources-create] placeholder text """ data = request.data.copy() fqdn = data.get("fqdn", None) name = data.get("name", None) domainname = data.get("domain", None) # If the user gave us fqdn and did not give us name/domain, expand # fqdn. if domainname is None and name is None and fqdn is not None: # Assume that we're working with an address, since we ignore # rrtype and rrdata. (name, domainname) = separate_fqdn(fqdn, "A") data["domain"] = domainname data["name"] = name # If the domain is a name, make it an id. if domainname is not None: if domainname.isdigit(): domain = Domain.objects.get_domain_or_404( domainname, user=request.user, perm=NodePermission.view ) else: domain = Domain.objects.get_domain_or_404( "name:%s" % domainname, user=request.user, perm=NodePermission.view, ) data["domain"] = domain.id form = DNSResourceForm(data=data, request=request) if form.is_valid(): return form.save() else: raise MAASAPIValidationError(form.errors)
def read(self, request): """@description-title List all DNS resource records @description List all DNS resource records. @param (string) "domain" [required=false] Restricts the listing to entries for the domain. @param (string) "name" [required=false] Restricts the listing to entries of the given name. @param (string) "rrtype" [required=false] Restricts the listing to entries which have records of the given rrtype. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing a list of the requested DNS resource record objects. @success-example "success-json" [exkey=dnsresourcerecords-read] placeholder text """ data = request.GET fqdn = data.get("fqdn", None) name = data.get("name", None) domainname = data.get("domain", None) rrtype = data.get("rrtype", None) if domainname is None and name is None and fqdn is not None: # We need a type for resource separation. If the user didn't give # us a rrtype, then assume it's an address of some sort. (name, domainname) = separate_fqdn(fqdn, rrtype) # If the domain is a name, make it an id. if domainname is not None: if domainname.isdigit(): domain = Domain.objects.get_domain_or_404( domainname, user=request.user, perm=NodePermission.view ) else: domain = Domain.objects.get_domain_or_404( "name:%s" % domainname, user=request.user, perm=NodePermission.view, ) query = DNSData.objects.filter( dnsresource__domain_id=domain.id ).order_by("dnsresource__name") else: query = DNSData.objects.all().order_by( "dnsresource__domain_id", "dnsresource__name" ) if name is not None: query = query.filter(dnsresource__name=name) if rrtype is not None: query = query.filter(rrtype=rrtype) return query
def read(self, request): """@description-title List resources @description List all resources for the specified criteria. @param (string) "domain" [required=false] Restricts the listing to entries for the domain. @param (string) "name" [required=false] Restricts the listing to entries of the given name. @param (string) "rrtype" [required=false] Restricts the listing to entries which have records of the given rrtype. @param (boolean) "all" [required=false] Include implicit DNS records created for nodes registered in MAAS if true. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing a list of the requested DNS resource objects. @success-example "success-json" [exkey=dnsresources-read] placeholder text @error (http-status-code) "404" 404 @error (content) "not-found" The requested DNS resources are not found. @error-example "not-found" Not Found """ data = request.GET fqdn = data.get("fqdn", None) name = data.get("name", None) domainname = data.get("domain", None) rrtype = data.get("rrtype", None) if rrtype is not None: rrtype = rrtype.upper() _all = get_optional_param( request.GET, "all", default=False, validator=StringBool ) if domainname is None and name is None and fqdn is not None: # We need a type for resource separation. If the user didn't give # us a rrtype, then assume it's an address of some sort. (name, domainname) = separate_fqdn(fqdn, rrtype) user = request.user return get_dnsresource_queryset(_all, domainname, name, rrtype, user)
def create(self, request): """Create a dnsresource. :param fqdn: Hostname (with domain) for the dnsresource. Either fqdn or (name, domain) must be specified. Fqdn is ignored if either name or domain is given. :param name: Hostname (without domain) :param domain: Domain (name or id) :param address_ttl: Default ttl for entries in this zone. :param ip_addresses: (optional) Address (ip or id) to assign to the dnsresource. """ data = request.data.copy() fqdn = data.get('fqdn', None) name = data.get('name', None) domainname = data.get('domain', None) # If the user gave us fqdn and did not give us name/domain, expand # fqdn. if domainname is None and name is None and fqdn is not None: # Assume that we're working with an address, since we ignore # rrtype and rrdata. (name, domainname) = separate_fqdn(fqdn, 'A') data['domain'] = domainname data['name'] = name # If the domain is a name, make it an id. if domainname is not None: if domainname.isdigit(): domain = Domain.objects.get_domain_or_404( domainname, user=request.user, perm=NODE_PERMISSION.VIEW) else: domain = Domain.objects.get_domain_or_404( "name:%s" % domainname, user=request.user, perm=NODE_PERMISSION.VIEW) data['domain'] = domain.id form = DNSResourceForm(data=data, request=request) if form.is_valid(): return form.save() else: raise MAASAPIValidationError(form.errors)
def create(self, request): """Create a DNS resource record. :param fqdn: Hostname (with domain) for the dnsresource. Either fqdn or (name, domain) must be specified. Fqdn is ignored if either name or domain is given (e.g. www.your-maas.maas). :param name: The name (or hostname without a domain) of the DNS resource record (e.g. www.your-maas) :param domain: The domain (name or id) where to create the DNS resource record (Domain (e.g. 'maas') :param rrtype: The resource record type (e.g 'cname', 'mx', 'ns', 'srv', 'sshfp', 'txt') :param rrdata: The resource record data (e.g. 'your-maas', '10 mail.your-maas.maas') """ data = request.data.copy() domain = None fqdn = data.get('fqdn', None) name = data.get('name', None) domainname = data.get('domain', None) rrtype = data.get('rrtype', None) rrdata = data.get('rrdata', None) if rrtype is None: raise MAASAPIBadRequest("rrtype must be provided.") if rrdata is None: raise MAASAPIBadRequest("rrdata must be provided.") # If the user gave us fqdn and did not give us name/domain, expand # fqdn. if domainname is None and name is None and fqdn is not None: # We need a type for resource separation. If the user didn't give # us a rrtype, then assume it's an address of some sort. rrtype = data.get('rrtype', None) (name, domainname) = separate_fqdn(fqdn, rrtype) data['domain'] = domainname data['name'] = name # If the domain is a name, make it an id. if domainname is not None: if domainname.isdigit(): domain = Domain.objects.get_domain_or_404( domainname, user=request.user, perm=NodePermission.view) else: domain = Domain.objects.get_domain_or_404( "name:%s" % domainname, user=request.user, perm=NodePermission.view) data['domain'] = domain.id if domain is None or name is None: raise MAASAPIValidationError( "Either name and domain (or fqdn) must be specified") # Do we already have a DNSResource for this fqdn? dnsrr = DNSResource.objects.filter(name=name, domain__id=domain.id) if not dnsrr.exists(): form = DNSResourceForm(data=data, request=request) if form.is_valid(): form.save() else: raise MAASAPIValidationError(form.errors) dnsrr = DNSResource.objects.filter(name=name, domain__id=domain.id) data['dnsresource'] = dnsrr form = DNSDataForm(data=data) if form.is_valid(): return form.save() else: raise MAASAPIValidationError(form.errors)
def add_delegations(self, mapping, ns_host_name, dns_ip_list, default_ttl): """Find any subdomains that need to be added to this domain, and add them. This function updates the mapping to add delegations and any needed glue records for any domains that are descendants of this one. These are not in the database, because they may be multi-lable (foo.bar.maas and maas are domains, but bar.maas isn't), and we don't want to allow multi-label elements in the model, due to the extreme complexity it introduces. """ # Recursive includes. from maasserver.models.dnsresource import separate_fqdn subdomains = Domain.objects.filter(name__endswith="." + self.name) possible = subdomains[:] # Anything with an intervening domain should not be delegated from # this domain. for middle in possible: subdomains = subdomains.exclude(name__endswith="." + middle.name) for subdomain in subdomains: nsttl = subdomain.get_base_ttl("NS", default_ttl) ttl = subdomain.get_base_ttl("A", default_ttl) # Strip off this domain name from the end of the resource name. name = subdomain.name[:-len(self.name) - 1] # If we are authoritative for the subdomain, then generate the NS # and any needed glue records. These will automatically be in the # child zone. if subdomain.authoritative: mapping[name].rrset.add((nsttl, "NS", ns_host_name)) if ns_host_name.endswith("." + self.name): # The ns_host_name lives in a subdomain of this subdomain, # and we are authoritative for that. We need to add glue # to this subdomain. ns_name = separate_fqdn(ns_host_name, "NS", self.name)[0] for addr in dns_ip_list: if IPAddress(addr).version == 4: mapping[ns_name].rrset.add((ttl, "A", addr)) else: mapping[ns_name].rrset.add((ttl, "AAAA", addr)) # Also return any NS RRset from the dnsdata for the '@' label in # that zone. Add glue records for NS hosts as needed. for lhs in subdomain.dnsresource_set.filter(name="@"): for data in lhs.dnsdata_set.filter(rrtype="NS"): mapping[name].rrset.add((ttl, data.rrtype, data.rrdata)) # Figure out if we need to add glue, and generate it if # needed. if data.rrdata == "@": # This glue is the responsibility of the admin. continue if not data.rrdata.endswith("."): # Non-qualified NSRR, append the domain. fqdn = "%s.%s." % (data.rrdata, subdomain.name) elif not data.rrdata.endswith("%s." % subdomain.name): continue else: # NSRR is an FQDN in or under subdomain. fqdn = data.rrdata # If we get here, then the NS host is in subdomain, or some # subdomain thereof, and is not '@' in the subdomain. # Strip the trailing dot, and split the FQDN. h_name, d_name = separate_fqdn(fqdn[:-1], "NS") # Make sure we look in the right domain for the addresses. if d_name == subdomain.name: nsrrset = subdomain.dnsresource_set.filter(name=h_name) else: nsdomain = Domain.objects.filter(name=d_name) if not nsdomain.exists(): continue else: nsdomain = nsdomain[0] nsrrset = nsdomain.dnsresource_set.filter(name=h_name) h_name = fqdn[:-len(subdomain.name) - 2] for nsrr in nsrrset: for addr in nsrr.get_addresses(): if IPAddress(addr).version == 4: mapping[h_name].rrset.add((ttl, "A", addr)) else: mapping[h_name].rrset.add((ttl, "AAAA", addr))
def create(self, request): """@description-title Create a DNS resource record @description Create a new DNS resource record. @param (string) "fqdn" [required=false] Hostname (with domain) for the dnsresource. Either ``fqdn`` or ``name`` and ``domain`` must be specified. ``fqdn`` is ignored if either name or domain is given (e.g. www.your-maas.maas). @param (string) "name" [required=false] The name (or hostname without a domain) of the DNS resource record (e.g. www.your-maas) @param (string) "domain" [required=false] The domain (name or id) where to create the DNS resource record (Domain (e.g. 'maas') @param (string) "rrtype" [required=false] The resource record type (e.g ``cname``, ``mx``, ``ns``, ``srv``, ``sshfp``, ``txt``). @param (string) "rrdata" [required=false] The resource record data (e.g. 'your-maas', '10 mail.your-maas.maas') @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing the new DNS resource record object. @success-example "success-json" [exkey=dnsresourcerecords-create] placeholder text """ data = request.data.copy() domain = None fqdn = data.get("fqdn", None) name = data.get("name", None) domainname = data.get("domain", None) rrtype = data.get("rrtype", None) rrdata = data.get("rrdata", None) if rrtype is None: raise MAASAPIBadRequest("rrtype must be provided.") if rrdata is None: raise MAASAPIBadRequest("rrdata must be provided.") # If the user gave us fqdn and did not give us name/domain, expand # fqdn. if domainname is None and name is None and fqdn is not None: # We need a type for resource separation. If the user didn't give # us a rrtype, then assume it's an address of some sort. rrtype = data.get("rrtype", None) (name, domainname) = separate_fqdn(fqdn, rrtype) data["domain"] = domainname data["name"] = name # If the domain is a name, make it an id. if domainname is not None: if domainname.isdigit(): domain = Domain.objects.get_domain_or_404( domainname, user=request.user, perm=NodePermission.view ) else: domain = Domain.objects.get_domain_or_404( "name:%s" % domainname, user=request.user, perm=NodePermission.view, ) data["domain"] = domain.id if domain is None or name is None: raise MAASAPIValidationError( "Either name and domain (or fqdn) must be specified" ) # Do we already have a DNSResource for this fqdn? dnsrr = DNSResource.objects.filter(name=name, domain__id=domain.id) if not dnsrr.exists(): form = DNSResourceForm(data=data, request=request) if form.is_valid(): form.save() else: raise MAASAPIValidationError(form.errors) dnsrr = DNSResource.objects.filter(name=name, domain__id=domain.id) data["dnsresource"] = dnsrr form = DNSDataForm(data=data) if form.is_valid(): return form.save() else: raise MAASAPIValidationError(form.errors)
def test_separate_fqdn_splits_nonsrv(self): self.assertEqual( ("foo", "test.example.com"), separate_fqdn("foo.test.example.com", 'A'))
def test_separate_fqdn_splits_srv(self): self.assertEqual( ("_sip._tcp.voip", "example.com"), separate_fqdn("_sip._tcp.voip.example.com", 'SRV'))
def _gen_forward_zones(domains, serial, ns_host_name, mappings, rrset_mappings, default_ttl): """Generator of forward zones, collated by domain name.""" dns_ip_list = get_dns_server_addresses() domains = set(domains) # For each of the domains that we are generating, create the zone from: # 1. Node: ip mapping(domain) (which includes dnsresource addresses). # 2. Dnsresource non-address records in this domain. # 3. For the default domain all forward look ups for the managed and # unmanaged dynamic ranges. for domain in domains: zone_ttl = default_ttl if domain.ttl is None else domain.ttl # 1. node: ip mapping(domain) # Map all of the nodes in this domain, including the user-reserved # ip addresses. Separate_fqdn handles top-of-domain names needing # to have the name '@', and we already know the domain name, so we # discard that part of the return. mapping = { separate_fqdn(hostname, domainname=domain.name)[0]: info for hostname, info in mappings[domain].items() } # 2a. Create non-address records. Specifically ignore any CNAME # records that collide with addresses in mapping. other_mapping = rrset_mappings[domain] # 2b. Capture NS RRsets for anything that is a child of this domain domain.add_delegations(other_mapping, ns_host_name, dns_ip_list, default_ttl) # 3. All of the special handling for the default domain. dynamic_ranges = [] if domain.is_default(): # 3a. All forward entries for the managed and unmanaged dynamic # ranges go into the default domain. subnets = Subnet.objects.all().prefetch_related("iprange_set") for subnet in subnets: # We loop through the whole set so the prefetch above works # in one query. for ip_range in subnet.iprange_set.all(): if ip_range.type == IPRANGE_TYPE.DYNAMIC: dynamic_ranges.append(ip_range.get_MAASIPRange()) # 3b. Add A/AAAA RRset for @. If glue is needed for any other # domain, adding the glue is the responsibility of the admin. ttl = domain.get_base_ttl('A', default_ttl) for dns_ip in dns_ip_list: if dns_ip.version == 4: other_mapping['@'].rrset.add( (ttl, 'A', dns_ip.format())) else: other_mapping['@'].rrset.add( (ttl, 'AAAA', dns_ip.format())) yield DNSForwardZoneConfig( domain.name, serial=serial, default_ttl=zone_ttl, ns_ttl=domain.get_base_ttl('NS', default_ttl), ipv4_ttl=domain.get_base_ttl('A', default_ttl), ipv6_ttl=domain.get_base_ttl('AAAA', default_ttl), mapping=mapping, ns_host_name=ns_host_name, other_mapping=other_mapping, dynamic_ranges=dynamic_ranges, )