igroykt / letsencrypt-nic

Приложение для выписки wildcard сертификатов посредством DNS challenge
BSD 3-Clause "New" or "Revised" License
15 stars 2 forks source link

api.records error: Unknown record type: PTR #6

Closed Koruel closed 1 year ago

Koruel commented 2 years ago

Приветствую. На этапе очистки, что при тестовом запуске, что при первом или повторном, получаю такое:

Configuring OAuth...
Authorize API...
Extract all DNS records...
Running manual-cleanup-hook command: /opt/lenicru/clean
Hook '--manual-cleanup-hook' for mydomain.com reported error code 1
Hook '--manual-cleanup-hook' for mydomain.com ran with error output:
 api.records error: Unknown record type: PTR
Hook '--manual-cleanup-hook' for mydomain.com ran with output:

При этом сертификат выпускается и обновляется, однако, TXT-шки после челенджа не очищаются, а скрипт валится с ошибкой. Судя по коду, сперва идёт забор всех записей в clean.py (74 строка). А полученный список передаётся в функцию Func.NIC_findTXTID из файла func/func.py чтобы отсеять только TXT и нужный текст. Но до отсеивания не доходит, валится именно api.records. В логе LE аналогичная запись:

  Configuring OAuth...
 Authorize API...
 Extract all DNS records...
2022-02-08 03:46:46,608:WARNING:certbot.display.ops:Hook '--manual-cleanup-hook' for cdek.ru ran with error output:
 api.records error: Unknown record type: PTR
2022-02-08 03:46:46,608:INFO:certbot.compat.misc:Running manual-cleanup-hook command: /opt/lenicru/clean
2022-02-08 03:46:47,397:WARNING:certbot.display.ops:Hook '--manual-cleanup-hook' for mydomain.com reported error code 1
2022-02-08 03:46:47,397:DEBUG:certbot._internal.display.obj:Notifying user: Hook '--manual-cleanup-hook' for mydomain.com ran with output:

Судя по коду никакие другие типы записей вообще не дёргаются, кроме TXT. Куда копать?

Koruel commented 2 years ago

Похоже сам и нашел проблему. https://github.com/andr1an/nic-api/blob/master/nic_api/models.py 48-49 строки - условие проверки типа записи по списку. а список там же, строка 36. Пока размышляю о том, как поступить.

Koruel commented 2 years ago

У меня Oracle Linux 8.5. Python 3.6.8. После установки всех зависимостей и перед билдом отредактировал файл /usr/local/lib/python3.6/site-packages/nic_api/models.py

"""nic_api - classes for entities returned by API."""

import sys
from xml.etree import ElementTree

# Python 2.7 compatibility
if sys.version_info.major < 3:
    _XML_ENCODING = 'utf-8'
else:
    _XML_ENCODING = 'unicode'

def _strtobool(string):
    """Converts a string from NIC API response to a bool."""
    return {'true': True, 'false': False}[string]

def parse_record(rr):
    """Parses record XML representation to one of DNSRecord subclasses.

    Reads <rr> tag, gets record type and passes <rr> internals to the specific
    DNS record model.

    Arguments:
        rr: an instance if ElementTree.Element: <rr> tag from API response;

    Returns:
        one of SOARecord, NSRecord, ARecord, AAAARecord, CNAMERecord, MXRecord,
        TXTRecord.
    """
    # TODO: move this to the DNSRecord class as "from_xml"
    if not isinstance(rr, ElementTree.Element):
        raise TypeError('"rr" must be an instance of ElementTree.Element')

    record_classes = {
        'SOA': SOARecord,
        'NS': NSRecord,
        'A': ARecord,
        'AAAA': AAAARecord,
        'CNAME': CNAMERecord,
        'MX': MXRecord,
        'TXT': TXTRecord,
        'PTR': PTRRecord,
    }

    record_type = rr.find('type').text

    if record_type not in record_classes:
        raise TypeError('Unknown record type: {}'.format(record_type))

    return record_classes[record_type].from_xml(rr)

# *****************************************************************************
# Model of service
#

class NICService(object):
    """Model of service object."""

    def __init__(self, admin, domains_limit, domains_num, enable, has_primary,
                 name, payer, tariff, rr_limit=None, rr_num=None):
        self.admin = admin
        self.domains_limit = int(domains_limit)
        self.domains_num = int(domains_num)
        self.enable = enable
        self.has_primary = has_primary
        self.name = name
        self.payer = payer
        if rr_limit is not None:
            self.rr_limit = int(rr_limit)
        if rr_num is not None:
            self.rr_num = int(rr_num)
        self.tariff = tariff

    def __repr__(self):
        return repr(vars(self))

    @classmethod
    def from_xml(cls, service):
        """Alternative constructor - creates an instance of NICService from
        its XML representation.
        """
        if not isinstance(service, ElementTree.Element):
            raise TypeError(
                '"service" must be an instance of ElementTree.Element')

        kwargs = {k.replace('-', '_'): v
                  for k, v in service.attrib.items()}
        kwargs['enable'] = _strtobool(kwargs['enable'])
        kwargs['has_primary'] = _strtobool(kwargs['has_primary'])
        return cls(**kwargs)

# *****************************************************************************
# Model of DNS zone
#

class NICZone(object):
    """Model of zone object."""

    def __init__(self, admin, enable, has_changes, has_primary, id_, idn_name,
                 name, payer, service):
        self.admin = admin
        self.enable = enable
        self.has_changes = has_changes
        self.has_primary = has_primary
        self.id = int(id_)
        self.idn_name = idn_name
        self.name = name
        self.payer = payer
        self.service = service

    def __repr__(self):
        return repr(vars(self))

    def to_xml(self):
        # TODO: add implementation if needed
        raise NotImplementedError('Not implemented!')

    @classmethod
    def from_xml(cls, zone):
        """Alternative constructor - creates an instance of NICZone from
        its XML representation.
        """
        if not isinstance(zone, ElementTree.Element):
            raise TypeError(
                '"zone" must be an instance of ElementTree.Element')

        kwargs = {k.replace('-', '_'): v
                  for k, v in zone.attrib.items()}

        kwargs['id_'] = kwargs['id']
        kwargs.pop('id')

        kwargs['enable'] = _strtobool(kwargs['enable'])
        kwargs['has_changes'] = _strtobool(kwargs['has_changes'])
        kwargs['has_primary'] = _strtobool(kwargs['has_primary'])
        return cls(**kwargs)

# *****************************************************************************
# Models of DNS records
#
# Each model has __init__() method that loads data into object by direct
# assigning and a class method from_xml() that constructs the object from
# an ElementTree.Element.
#
# Each model has to_xml() method that returns (str) an XML representation
# of the current record.
#

class DNSRecord(object):
    """Base model of NIC.RU DNS record."""

    def __init__(self, id_=None, name='', idn_name=None):
        if id_ is None:
            self.id = id_
        else:
            self.id = int(id_)
        if self.id == 0:
            raise ValueError('Invalid record ID!')
        self.name = name
        self.idn_name = idn_name if idn_name else name

    def __repr__(self):
        return repr(vars(self))

class SOARecord(DNSRecord):
    """Model of SOA record."""

    def __init__(self, serial, refresh, retry, expire, minimum, mname, rname,
                 **kwargs):
        super(SOARecord, self).__init__(**kwargs)
        self.serial = int(serial)
        self.refresh = int(refresh)
        self.retry = int(retry)
        self.expire = int(expire)
        self.minimum = int(minimum)
        self.mname = DNSRecord(**mname)
        self.rname = DNSRecord(**rname)

    def to_xml(self):
        # TODO: add implementation if needed
        raise NotImplementedError('Not implemented!')

    @classmethod
    def from_xml(cls, rr):
        """Alternative constructor - creates an instance of SOARecord from
        its XML representation.
        """
        if not isinstance(rr, ElementTree.Element):
            raise TypeError('"rr" must be an instance of ElementTree.Element')
        if rr.find('type').text != 'SOA':
            raise ValueError('Record is not a SOA record!')

        id_ = rr.attrib['id'] if 'id' in rr.attrib else None
        name = rr.find('name').text
        idn_name = rr.find('idn-name').text
        soa_fields = {
            elem: rr.find('soa/' + elem).text
            for elem in ('serial', 'refresh', 'retry', 'expire', 'minimum')
        }
        soa_fields['mname'] = {elem.tag.replace('-', '_'): elem.text
                               for elem in rr.findall('soa/mname/*')}
        soa_fields['rname'] = {elem.tag.replace('-', '_'): elem.text
                               for elem in rr.findall('soa/rname/*')}
        return cls(id_=id_, name=name, idn_name=idn_name, **soa_fields)

class NSRecord(DNSRecord):
    """Model of NS record."""

    def __init__(self, ns, **kwargs):
        super(NSRecord, self).__init__(**kwargs)
        self.ns = ns

    def to_xml(self):
        # TODO: add implementation if needed
        raise NotImplementedError('Not implemented!')

    @classmethod
    def from_xml(cls, rr):
        """Alternative constructor - creates an instance of NSRecord from
        its XML representation.
        """
        if not isinstance(rr, ElementTree.Element):
            raise TypeError('"rr" must be an instance of ElementTree.Element')
        if rr.find('type').text != 'NS':
            raise ValueError('Record is not a NS record!')

        id_ = rr.attrib['id'] if 'id' in rr.attrib else None
        name = rr.find('name').text
        idn_name = rr.find('idn-name').text
        ns = rr.find('ns/name').text
        return cls(id_=id_, name=name, idn_name=idn_name, ns=ns)

class ARecord(DNSRecord):
    """Model of A record."""

    ttl = None

    def __init__(self, a, ttl=None, **kwargs):
        super(ARecord, self).__init__(**kwargs)
        if ttl is not None:
            self.ttl = int(ttl)
            if self.ttl == 0:
                raise ValueError('Invalid A TTL!')
        self.a = a

    def to_xml(self):
        """Returns an XML representation of record object."""
        root = ElementTree.Element('rr')
        if self.id:
            root.attrib['id'] = self.id
        _name = ElementTree.SubElement(root, 'name')
        _name.text = self.name
        if self.ttl is not None:
            _ttl = ElementTree.SubElement(root, 'ttl')
            _ttl.text = str(self.ttl)
        _type = ElementTree.SubElement(root, 'type')
        _type.text = 'A'
        _a = ElementTree.SubElement(root, 'a')
        _a.text = self.a
        return ElementTree.tostring(root, encoding=_XML_ENCODING)

    @classmethod
    def from_xml(cls, rr):
        """Alternative constructor - creates an instance of ARecord from
        its XML representation.
        """
        if not isinstance(rr, ElementTree.Element):
            raise TypeError('"rr" must be an instance of ElementTree.Element')
        if rr.find('type').text != 'A':
            raise ValueError('Record is not an A record!')

        id_ = rr.attrib['id'] if 'id' in rr.attrib else None
        name = rr.find('name').text
        idn_name = rr.find('idn-name').text
        elem_ttl = rr.find('ttl')
        ttl = elem_ttl.text if elem_ttl is not None else None
        a = rr.find('a').text
        return cls(id_=id_, name=name, idn_name=idn_name, ttl=ttl, a=a)

class AAAARecord(DNSRecord):
    """Model of AAAA record."""

    ttl = None

    def __init__(self, aaaa, ttl=None, **kwargs):
        super(AAAARecord, self).__init__(**kwargs)
        if ttl is not None:
            self.ttl = int(ttl)
            if self.ttl == 0:
                raise ValueError('Invalid AAAA TTL!')
        self.aaaa = aaaa

    def to_xml(self):
        """Returns an XML representation of record object."""
        root = ElementTree.Element('rr')
        if self.id:
            root.attrib['id'] = self.id
        _name = ElementTree.SubElement(root, 'name')
        _name.text = self.name
        if self.ttl is not None:
            _ttl = ElementTree.SubElement(root, 'ttl')
            _ttl.text = str(self.ttl)
        _type = ElementTree.SubElement(root, 'type')
        _type.text = 'AAAA'
        _aaaa = ElementTree.SubElement(root, 'aaaa')
        _aaaa.text = self.aaaa
        return ElementTree.tostring(root, encoding=_XML_ENCODING)

    @classmethod
    def from_xml(cls, rr):
        """Alternative constructor - creates an instance of AAAARecord from
        its XML representation.
        """
        if not isinstance(rr, ElementTree.Element):
            raise TypeError('"rr" must be an instance of ElementTree.Element')
        if rr.find('type').text != 'AAAA':
            raise ValueError('Record is not an AAAA record!')

        id_ = rr.attrib['id'] if 'id' in rr.attrib else None
        name = rr.find('name').text
        idn_name = rr.find('idn-name').text
        elem_ttl = rr.find('ttl')
        ttl = elem_ttl.text if elem_ttl is not None else None
        aaaa = rr.find('aaaa').text
        return cls(id_=id_, name=name, idn_name=idn_name, ttl=ttl, aaaa=aaaa)

class CNAMERecord(DNSRecord):
    """Model of CNAME record."""

    ttl = None

    def __init__(self, cname, ttl=None, **kwargs):
        super(CNAMERecord, self).__init__(**kwargs)
        if ttl is not None:
            self.ttl = int(ttl)
            if self.ttl == 0:
                raise ValueError('Invalid CNAME TTL!')
        self.cname = cname

    def to_xml(self):
        """Returns an XML representation of record object."""
        root = ElementTree.Element('rr')
        if self.id:
            root.attrib['id'] = self.id
        _name = ElementTree.SubElement(root, 'name')
        _name.text = self.name
        if self.ttl is not None:
            _ttl = ElementTree.SubElement(root, 'ttl')
            _ttl.text = str(self.ttl)
        _type = ElementTree.SubElement(root, 'type')
        _type.text = 'CNAME'
        _cname = ElementTree.SubElement(root, 'cname')
        _cname_name = ElementTree.SubElement(_cname, 'name')
        _cname_name.text = self.cname
        return ElementTree.tostring(root, encoding=_XML_ENCODING)

    @classmethod
    def from_xml(cls, rr):
        """Alternative constructor - creates an instance of CNAMERecord from
        its XML representation.
        """
        if not isinstance(rr, ElementTree.Element):
            raise TypeError('"rr" must be an instance of ElementTree.Element')
        if rr.find('type').text != 'CNAME':
            raise ValueError('Record is not a CNAME record!')

        id_ = rr.attrib['id'] if 'id' in rr.attrib else None
        name = rr.find('name').text
        idn_name = rr.find('idn-name').text
        elem_ttl = rr.find('ttl')
        ttl = elem_ttl.text if elem_ttl is not None else None
        cname = rr.find('cname/name').text
        return cls(id_=id_, name=name, idn_name=idn_name, ttl=ttl, cname=cname)

class MXRecord(DNSRecord):
    """Model of MX record."""

    ttl = None

    def __init__(self, preference, exchange, ttl=None, **kwargs):
        super(MXRecord, self).__init__(**kwargs)
        if ttl is not None:
            self.ttl = int(ttl)
            if self.ttl == 0:
                raise ValueError('Invalid MX TTL!')
        self.preference = int(preference)
        self.exchange = exchange

    def to_xml(self):
        # TODO: add implementation if needed
        raise NotImplementedError('Not implemented!')

    @classmethod
    def from_xml(cls, rr):
        """Alternative constructor - creates an instance of MXRecord from
        its XML representation.
        """
        if not isinstance(rr, ElementTree.Element):
            raise TypeError('"rr" must be an instance of ElementTree.Element')
        if rr.find('type').text != 'MX':
            raise ValueError('Record is not an MX record!')

        id_ = rr.attrib['id'] if 'id' in rr.attrib else None
        name = rr.find('name').text
        idn_name = rr.find('idn-name').text
        elem_ttl = rr.find('ttl')
        ttl = elem_ttl.text if elem_ttl is not None else None
        preference = rr.find('mx/preference').text
        exchange = rr.find('mx/exchange/name').text
        return cls(id_=id_, name=name, idn_name=idn_name, ttl=ttl,
                   preference=preference, exchange=exchange)

class TXTRecord(DNSRecord):
    """Model of TXT record."""

    ttl = None

    def __init__(self, txt, ttl=None, **kwargs):
        super(TXTRecord, self).__init__(**kwargs)
        if ttl is not None:
            self.ttl = int(ttl)
            if self.ttl == 0:
                raise ValueError('Invalid TXT TTL!')
        self.txt = txt

    def to_xml(self):
        """Returns an XML representation of record object."""
        root = ElementTree.Element('rr')
        if self.id:
            root.attrib['id'] = self.id
        _name = ElementTree.SubElement(root, 'name')
        _name.text = self.name
        if self.ttl is not None:
            _ttl = ElementTree.SubElement(root, 'ttl')
            _ttl.text = str(self.ttl)
        _type = ElementTree.SubElement(root, 'type')
        _type.text = 'TXT'
        _txt = ElementTree.SubElement(root, 'txt')
        _txt_string = ElementTree.SubElement(_txt, 'string')
        _txt_string.text = self.txt
        return ElementTree.tostring(root, encoding=_XML_ENCODING)

    @classmethod
    def from_xml(cls, rr):
        """Alternative constructor - creates an instance of TXTRecord from
        its XML representation.
        """
        if not isinstance(rr, ElementTree.Element):
            raise TypeError('"rr" must be an instance of ElementTree.Element')
        if rr.find('type').text != 'TXT':
            raise ValueError('Record is not a TXT record!')

        id_ = rr.attrib['id'] if 'id' in rr.attrib else None
        name = rr.find('name').text
        idn_name = rr.find('idn-name').text
        elem_ttl = rr.find('ttl')
        ttl = elem_ttl.text if elem_ttl is not None else None
        txt = [string.text for string in rr.findall('txt/string')]
        if len(txt) == 1:
            txt = txt[0]
        return cls(id_=id_, name=name, idn_name=idn_name, ttl=ttl, txt=txt)

class PTRRecord(DNSRecord):
    """Model of PTR record."""

    ttl = None

    def __init__(self, txt, ttl=None, **kwargs):
        super(PTRRecord, self).__init__(**kwargs)
        if ttl is not None:
            self.ttl = int(ttl)
            if self.ttl == 0:
                raise ValueError('Invalid PTR TTL!')
        self.txt = txt

    def to_xml(self):
        """Returns an XML representation of record object."""
        root = ElementTree.Element('rr')
        if self.id:
            root.attrib['id'] = self.id
        _name = ElementTree.SubElement(root, 'name')
        _name.text = self.name
        if self.ttl is not None:
            _ttl = ElementTree.SubElement(root, 'ttl')
            _ttl.text = str(self.ttl)
        _type = ElementTree.SubElement(root, 'type')
        _type.text = 'TXT'
        _txt = ElementTree.SubElement(root, 'txt')
        _txt_string = ElementTree.SubElement(_txt, 'string')
        _txt_string.text = self.txt
        return ElementTree.tostring(root, encoding=_XML_ENCODING)

    @classmethod
    def from_xml(cls, rr):
        """Alternative constructor - creates an instance of PTRRecord from
        its XML representation.
        """
        if not isinstance(rr, ElementTree.Element):
            raise TypeError('"rr" must be an instance of ElementTree.Element')
        if rr.find('type').text != 'PTR':
            raise ValueError('Record is not a PTR record!')

        id_ = rr.attrib['id'] if 'id' in rr.attrib else None
        name = rr.find('name').text
        idn_name = rr.find('idn-name').text
        elem_ttl = rr.find('ttl')
        ttl = elem_ttl.text if elem_ttl is not None else None
        txt = [string.text for string in rr.findall('txt/string')]
        if len(txt) == 1:
            txt = txt[0]
        return cls(id_=id_, name=name, idn_name=idn_name, ttl=ttl, txt=txt)

После этого всё заработало.

igroykt commented 2 years ago

Приветствую. На этапе очистки, что при тестовом запуске, что при первом или повторном, получаю такое:

Configuring OAuth...
Authorize API...
Extract all DNS records...
Running manual-cleanup-hook command: /opt/lenicru/clean
Hook '--manual-cleanup-hook' for mydomain.com reported error code 1
Hook '--manual-cleanup-hook' for mydomain.com ran with error output:
 api.records error: Unknown record type: PTR
Hook '--manual-cleanup-hook' for mydomain.com ran with output:

При этом сертификат выпускается и обновляется, однако, TXT-шки после челенджа не очищаются, а скрипт валится с ошибкой. Судя по коду, сперва идёт забор всех записей в clean.py (74 строка). А полученный список передаётся в функцию Func.NIC_findTXTID из файла func/func.py чтобы отсеять только TXT и нужный текст. Но до отсеивания не доходит, валится именно api.records. В логе LE аналогичная запись:

  Configuring OAuth...
 Authorize API...
 Extract all DNS records...
2022-02-08 03:46:46,608:WARNING:certbot.display.ops:Hook '--manual-cleanup-hook' for cdek.ru ran with error output:
 api.records error: Unknown record type: PTR
2022-02-08 03:46:46,608:INFO:certbot.compat.misc:Running manual-cleanup-hook command: /opt/lenicru/clean
2022-02-08 03:46:47,397:WARNING:certbot.display.ops:Hook '--manual-cleanup-hook' for mydomain.com reported error code 1
2022-02-08 03:46:47,397:DEBUG:certbot._internal.display.obj:Notifying user: Hook '--manual-cleanup-hook' for mydomain.com ran with output:

Судя по коду никакие другие типы записей вообще не дёргаются, кроме TXT. Куда копать?

Привет. Спасибо за фидбек. Щас сильно занят на основной работе, но коммит возможно решающий проблему уже сделал в ветку dev. Еще не тестировал. Судя по всему проблема в библиотеке nic-api, но исходя из того что там много чего не учтено (у самого была проблема с ipv6) думаю сделать воркараунд для обхода ошибок с стороны этой библиотеки. Поскольку ты фактически написал патч, то не плохо было бы сделать pull request в репозитории nic-api :)

igroykt commented 1 year ago

@Koruel приветствую. Забыл отписаться. 17 сентября пофиксили это дело в библиотеке nic-api. Таск закрываю.