diff --git a/HISTORY.rst b/HISTORY.rst index f4a1514..80794e2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,299 +1,334 @@ Release History --------------- + +1.8.6 (2023-03-22) +++++++++++++++++++ +- Better python3 compatibility + +1.8.0 (2017-07-31) +++++++++++++++++++ +- CAA Record support +- Exponential backoff on blocked tasks +- Misc bug fixes + + +1.7.10 (2016-12-08) ++++++++++++++++++++ +- TSIG names can now be attached to `dyn.tm.zones.ExternalNamserver` + +1.7.9 (2016-11-28) +++++++++++++++++++ +- Add publish parameter to delete methods +- Check publish and implicitPublish for delete + + +1.7.8 (2016-10-31) +++++++++++++++++++ +- Minor Bugfixes to ExtNameServer. +- Advanced Redirect SDK calls (Beta) + + +1.7.7 (2016-10-20) +++++++++++++++++++ +- Add 'all_ds' field to DNSSECKey class for multi-DS support +- Add `dyn.tm.zones.ExternalNamserver` and `dyn.tm.zones.ExternalNameserverEntry` + +1.7.6 (2016-09-29) +++++++++++++++++++ +- Bug fix for DSFNode export +- Internal Improvement for multiprocess applications. + 1.7.5 (2016-09-09) ++++++++++++++++++ -*Bug fix for token renewal on connection timeout +- Bug fix for token renewal on connection timeout 1.7.4 (2016-08-30) ++++++++++++++++++ -*Added IPACL feature to accounts library. Customers can now control Account Login ACLs for API and Web independently. +- Added IPACL feature to accounts library. Customers can now control Account Login ACLs for API and Web independently. 1.7.2 (2016-08-17) ++++++++++++++++++ -*Update add_node and remove_node functions in Traffic Director to respect implicitPublish setting. -*Updated permission fixes dyn.tm.accounts +- Update add_node and remove_node functions in Traffic Director to respect implicitPublish setting. +- Updated permission fixes dyn.tm.accounts 1.7.1 (2016-08-11) ++++++++++++++++++ -*task_id is now returned for a number of calls that spawn system tasks -*Bug fixes in RTTM. If you are using the RTTM with the SDK against our system, after Release-5.2.17 you will have to upgrade to this version. +- task_id is now returned for a number of calls that spawn system tasks +- Bug fixes in RTTM. If you are using the RTTM with the SDK against our system, after Release-5.2.17 you will have to upgrade to this version. 1.7.0 (2016-06-22) ++++++++++++++++++ -*Bug fix for implicit publishes in the DSF service with Publish Notes. -*Added `dyn.tm.dsf.DSFNode` Node type for DSF services. `dyn.tm.dsf.TrafficDirector` should now accept this node type, - as well as produce it. Regular `dyn.tm.zones.Node` objects can still be passed in, but they will no longer be generated - as output. -*Python3 compatability fixes as well as a through linting. +- Bug fix for implicit publishes in the DSF service with Publish Notes. +- Added `dyn.tm.dsf.DSFNode` Node type for DSF services. `dyn.tm.dsf.TrafficDirector` should now accept this node type, as well as produce it. Regular `dyn.tm.zones.Node` objects can still be passed in, but they will no longer be generated as output. +- Python3 compatability fixes as well as a through linting. 1.6.4 (2016-05-20) ++++++++++++++++++ -*Added Publish Notes to Traffic Director Service. User created Zone Notes can now be generated on a Traffic Director - Publish. This includes in line setter publishes, or full Service level publishes. +- Added Publish Notes to Traffic Director Service. User created Zone Notes can now be generated on a Traffic Director Publish. This includes in line setter publishes, or full Service level publishes. 1.6.3 (2016-3-21) -++++++++++++++++ -*Added TrafficDirector:replace_all_rulesets to wholesale replace rulesets on a TrafficDirector -*Added TrafficDirector:replace_one_ruleset to remove and replace a single ruleset entry in place -*Merged Proxy support from PR #73 ++++++++++++++++++ +- Added TrafficDirector:replace_all_rulesets to wholesale replace rulesets on a TrafficDirector +- Added TrafficDirector:replace_one_ruleset to remove and replace a single ruleset entry in place +- Merged Proxy support from PR #73 1.6.2 (2016-3-7) ++++++++++++++++ -*Added order_rulesets() to TrafficDirector object, for re-ordering Rulesets -*Added index=n to Ruleset create() so New rulesets can be placed in a certain location in the chain. -*Added getters for single DSF objects get_record(), get_record_set() etc. -*Fixed bug with DSF Monitor options -*Fixed bug where adding criteria to rulesets with 'always' criteria_type changes it to 'geoip' by default. +- Added order_rulesets() to TrafficDirector object, for re-ordering Rulesets +- Added index=n to Ruleset create() so New rulesets can be placed in a certain location in the chain. +- Added getters for single DSF objects get_record(), get_record_set() etc. +- Fixed bug with DSF Monitor options +- Fixed bug where adding criteria to rulesets with 'always' criteria_type changes it to 'geoip' by default. 1.6.1 (2016-2-11) +++++++++++++++++ -*Added UNKNOWN record type -*DSF records status getter added +- Added UNKNOWN record type +- DSF records status getter added 1.6.0 (2016-1-28) +++++++++++++++++ -*DSF service objects can now be independently Created, Updated, Read, and Deleted. -*Signifigant changes to how DSF service works. There may be some minor breaking changes here. -*Record getters now automatically pull data from system instead of storing them locally. +- DSF service objects can now be independently Created, Updated, Read, and Deleted. +- Signifigant changes to how DSF service works. There may be some minor breaking changes here. +- Record getters now automatically pull data from system instead of storing them locally. 1.5.2 (2016-1-11) +++++++++++++++++ -*Addition of Delay feature to GSLB Services -*Minor Improvements to GSLB features. -*Addition of Apex Finder +- Addition of Delay feature to GSLB Services +- Minor Improvements to GSLB features. +- Addition of Apex Finder 1.5.1 (2015-12-17) ++++++++++++++++++ -*Addition of CSYNC records +- Addition of CSYNC records 1.5.0 (2015-12-14) ++++++++++++++++++ -*Alias Traffic Director Support, coincides with ALIAS product release. -*Addition of CDS and CDNSKEY records. +- Alias Traffic Director Support, coincides with ALIAS product release. +- Addition of CDS and CDNSKEY records. 1.4.5 (2015-12-9) +++++++++++++++++ -* Added support for new syslog delivery type. `syslog_delivery` where `all` delivers messages no matter what the state and `change` only does so upon a detected change. +- Added support for new syslog delivery type. `syslog_delivery` where `all` delivers messages no matter what the state and `change` only does so upon a detected change. 1.4.4 (2015-11-25) ++++++++++++++++++ -* Added support for ALIAS records. +- Added support for ALIAS records. 1.4.3 (2015-08-14) ++++++++++++++++++ -*Added support for configurable Syslog Messages +- Added support for configurable Syslog Messages 1.4.2 (2015-08-10) ++++++++++++++++++ -* Added support for deleting all records of a certain type per #47. Thanks @tarokkk -* Exception classes are now based on `Exception` per #51. Thanks @thedebugger -* Fixed potential circular dependency in `dyn.tm.services` -* Added HTTP response debug logging +- Added support for deleting all records of a certain type per #47. Thanks @tarokkk +- Exception classes are now based on `Exception` per #51. Thanks @thedebugger +- Fixed potential circular dependency in `dyn.tm.services` +- Added HTTP response debug logging 1.4.1 (2015-07-23) ++++++++++++++++++ -*added zone notes at publish capabilities. -*added TSIG support +- added zone notes at publish capabilities. +- added TSIG support 1.4.0 (2015-06-26) ++++++++++++++++++ -*Added better coverage for passing Node Objects -*New way of handling DSFNodes with new API call +- Added better coverage for passing Node Objects +- New way of handling DSFNodes with new API call 1.3.14 (2015-06-22) +++++++++++++++++++ -* Internal fixes with zone. +- Internal fixes with zone. 1.3.13 (2015-06-15) +++++++++++++++++++ -*DSF Ruleset Feature enhancement +- DSF Ruleset Feature enhancement 1.3.12 (2015-06-03) +++++++++++++++++++ -*Added active properties for secondary zones. +- Added active properties for secondary zones. 1.3.4 (2014-11-11) ++++++++++++++++++ -* Bugfix for MMSesion JSON Error caused by arg filtering -* Bugfix for DSFRecord Creation on DSF GET calls +- Bugfix for MMSesion JSON Error caused by arg filtering +- Bugfix for DSFRecord Creation on DSF GET calls 1.3.3 (2014-10-26) ++++++++++++++++++ -* Fixed the majority of warnings when building docs, per issue #18 -* Added `dyn.tm.zones.get_all_secondary_zones` function for retrieving all secondary zones for an account +- Fixed the majority of warnings when building docs, per issue #18 +- Added `dyn.tm.zones.get_all_secondary_zones` function for retrieving all secondary zones for an account 1.3.2 (2014-10-21) ++++++++++++++++++ -* Fixed an issue where attempting to access a Zone's serial resulted in always performing a GET call +- Fixed an issue where attempting to access a Zone's serial resulted in always performing a GET call 1.3.1 (2014-10-16) ++++++++++++++++++ -* Adding additional hooks to dyn.tm.errors that return collections of exceptions +- Adding additional hooks to dyn.tm.errors that return collections of exceptions 1.3.0 (2014-10-14) ++++++++++++++++++ -* dyn.tm.session.DynectSession now accepts a `history` flag to enable per-session history recording +- dyn.tm.session.DynectSession now accepts a `history` flag to enable per-session history recording 1.2.0 (2014-09-29) ++++++++++++++++++ -* Addition of dyn.tm.tools module -* Added change_ip and map_ip functions to dyn.tm.tools -* Added __enter__ and __exit__ methods to DynectSession for allow for use as a context manager -* Added dyn.core.SessionEngine.new_session classmethod for forcing new session generation +- Addition of dyn.tm.tools module +- Added change_ip and map_ip functions to dyn.tm.tools +- Added __enter__ and __exit__ methods to DynectSession for allow for use as a context manager +- Added dyn.core.SessionEngine.new_session classmethod for forcing new session generation 1.1.0 (2014-09-16) ++++++++++++++++++ -* Internally improved Python2/3 compaability with the intoduction of the dyn.compat module -* Timestamps for various report types are accepted as Python datetime.datetime instances -* Added qps report access to Zones -* Added __str__, __repr__, __unicode__, and __bytes__ methods to all API object types -* Added conditional password encryption to allow for better in-app security -* Added the ability for users to specify their own password encryption keys -* Added __getstate__ and __setstate__ methods to SessionEngine, allowing sessions to be serialized -* Misc bug fixes +- Internally improved Python2/3 compaability with the intoduction of the dyn.compat module +- Timestamps for various report types are accepted as Python datetime.datetime instances +- Added qps report access to Zones +- Added __str__, __repr__, __unicode__, and __bytes__ methods to all API object types +- Added conditional password encryption to allow for better in-app security +- Added the ability for users to specify their own password encryption keys +- Added __getstate__ and __setstate__ methods to SessionEngine, allowing sessions to be serialized +- Misc bug fixes 1.0.3 (2014-09-05) ++++++++++++++++++ -* Adding changes provided by @thomasco to allow for GSLB monitor replacements +- Adding changes provided by @thomasco to allow for GSLB monitor replacements 1.0.2 (2014-08-26) ++++++++++++++++++ -* Added reports module -* Updated installation documentation +- Added reports module +- Updated installation documentation 1.0.1 (2014-08-06) ++++++++++++++++++ -* Small bugfix for an issue affecting sending EMails via the HTMLEmail class +- Small bugfix for an issue affecting sending EMails via the HTMLEmail class 1.0.0 (2014-08-05) ++++++++++++++++++ -* Revamed how sessions are structured to support the new SessionEngine interface -* Message Management is now out of BETA due to many bug fixes and additional testing -* You can now have one SessionEngine instance (Singleton) per Thread -* Added File Encoding definitions to source code -* Updated dyn.mm docs to actually include code samples -* Adding some general information on sessions, primarily for my own sanity -* Added EMail subclasses for easier formatting/sending of EMail messages -* mm.session.session and tm.session.session functions have been replaced by the SessionEngine get_session class method -* Completed the dyn.mm.reports module -* Misc MM related bug fixes +- Revamed how sessions are structured to support the new SessionEngine interface +- Message Management is now out of BETA due to many bug fixes and additional testing +- You can now have one SessionEngine instance (Singleton) per Thread +- Added File Encoding definitions to source code +- Updated dyn.mm docs to actually include code samples +- Adding some general information on sessions, primarily for my own sanity +- Added EMail subclasses for easier formatting/sending of EMail messages +- mm.session.session and tm.session.session functions have been replaced by the SessionEngine get_session class method +- Completed the dyn.mm.reports module +- Misc MM related bug fixes 0.9.11 (2014-07-25) +++++++++++++++++++ -* Fixed a bug with how calls to ``get_all_zones`` created ``Zone`` objects -* Tackled a possible bug also stemming from ``get_all_zones`` calls where a ``Zone``'s ``contact`` and ``ttl`` attributes could always be ``None`` +- Fixed a bug with how calls to ``get_all_zones`` created ``Zone`` objects +- Tackled a possible bug also stemming from ``get_all_zones`` calls where a ``Zone``'s ``contact`` and ``ttl`` attributes could always be ``None`` 0.9.10 (2014-07-07) +++++++++++++++++++ -* Added fix for potentially improperly formatted search parameters in dyn.tm.accounts.get_users +- Added fix for potentially improperly formatted search parameters in dyn.tm.accounts.get_users 0.9.9 (2014-06-26) ++++++++++++++++++ -* Added SecondaryZone delete method -* Added better User __str__ representations -* Added SOA TTL bug fix +- Added SecondaryZone delete method +- Added better User __str__ representations +- Added SOA TTL bug fix 0.9.6 (2014-05-16) ++++++++++++++++++ -* Added Zone attribute updating -* Misc Bug fixes for Python 2.x/3.x cross-compatibility -* GSLB _build bug fix +- Added Zone attribute updating +- Misc Bug fixes for Python 2.x/3.x cross-compatibility +- GSLB _build bug fix 0.9.5 (2014-05-12) ++++++++++++++++++ -* Added custom User-Agent to DynectSession -* Added __all__ attributes where appropriate to simplify imports -* Improved dyn.tm.services import structure +- Added custom User-Agent to DynectSession +- Added __all__ attributes where appropriate to simplify imports +- Improved dyn.tm.services import structure 0.9.3 (2014-05-08) ++++++++++++++++++ -* Added Active class type for all TM services -* Misc DSFMonitor/Record bug fixes -* Added DSFMonitorEndpoint class +- Added Active class type for all TM services +- Misc DSFMonitor/Record bug fixes +- Added DSFMonitorEndpoint class 0.8.0 (2014-05-08) ++++++++++++++++++ -* Integrated _APILists into GSLB and RTTM services -* Added a more intuitive polling solution for Zone XFERs +- Integrated _APILists into GSLB and RTTM services +- Added a more intuitive polling solution for Zone XFERs 0.7.0 (2014-05-02) ++++++++++++++++++ -* Fixed Notifier URI construction -* Added _APIDict and _APIList implementations to improve seamless updating of services -* Added custom DSF Record Type Objects to greatly improve ease of creation/management of DSF Services +- Fixed Notifier URI construction +- Added _APIDict and _APIList implementations to improve seamless updating of services +- Added custom DSF Record Type Objects to greatly improve ease of creation/management of DSF Services 0.6.0 (2014-04-23) ++++++++++++++++++ -* Fixed Python 3.x support with singleton DynectSession instance -* Finished implementation of dyn.mm.accounts -* Improved RTTM support -* Added Zone XFER support -* Added code examples to documentation -* Added better Geo TM support including custom Geo Record Type objects +- Fixed Python 3.x support with singleton DynectSession instance +- Finished implementation of dyn.mm.accounts +- Improved RTTM support +- Added Zone XFER support +- Added code examples to documentation +- Added better Geo TM support including custom Geo Record Type objects 0.5.0 (2014-04-07) ++++++++++++++++++ -* Added initial pass at Message Management BETA functionality -* Cleaned up exception raising and general logic involving internal exception handling +- Added initial pass at Message Management BETA functionality +- Cleaned up exception raising and general logic involving internal exception handling 0.4.0 (2014-03-25) ++++++++++++++++++ -* Initial fork of Cole Tuininga's code base -* Began creation of OO models -* General cleanup of .pyc files +- Initial fork of Cole Tuininga's code base +- Began creation of OO models +- General cleanup of .pyc files 0.3.0 (2012-10-05) ++++++++++++++++++ -* Updated by Cole Tuininga -* Compatibility update to work with Python 3, incorporating patches suggested by Jonathan Kamens -* Added a newline to debug output when polling for a result +- Updated by Cole Tuininga +- Compatibility update to work with Python 3, incorporating patches suggested by Jonathan Kamens +- Added a newline to debug output when polling for a result 0.2.0 (2012-05-27) ++++++++++++++++++ -* Updated by Cole Tuininga -* Minor reorg to make it easier to add the library to PyPI +- Updated by Cole Tuininga +- Minor reorg to make it easier to add the library to PyPI 0.1.0 (2011-10-08) ++++++++++++++++++ -* Updated by Cole Tuininga -* Initial release +- Updated by Cole Tuininga +- Initial release diff --git a/docs/tm/records/records.rst b/docs/tm/records/records.rst index 1eaa080..f9a83e1 100644 --- a/docs/tm/records/records.rst +++ b/docs/tm/records/records.rst @@ -18,6 +18,12 @@ ALIASRecord :members: :undoc-members: +CAARecord +========== +.. autoclass:: dyn.tm.records.CAARecord + :members: + :undoc-members: + CERTRecord ========== .. autoclass:: dyn.tm.records.CERTRecord diff --git a/dyn/__init__.py b/dyn/__init__.py index d547996..2ee048f 100644 --- a/dyn/__init__.py +++ b/dyn/__init__.py @@ -5,12 +5,17 @@ Requires Python 2.6 or higher, or the "simplejson" package. """ -version_info = (1, 7, 5) +version_info = (1, 8, 6) __name__ = 'dyn' __doc__ = 'A python wrapper for the DynDNS and DynEmail APIs' -__author__ = 'Jonathan Nappi, Cole Tuininga, Marc Howes, Philip Andrews' +__author__ = ''' + Jonathan Nappi, + Cole Tuininga, + Marc Howes, + Philip Andrews, + Robert Northover''' __version__ = '.'.join([str(x) for x in version_info]) -__maintainer__ = 'Marc Howes' -__email__ = 'mhowes@dyn.com' +__maintainer__ = 'Robert Northover' +__email__ = 'rnorthover@dyn.com' __status__ = 'Stable' __title__ = '{0} version {1}'.format(__name__, __version__) diff --git a/dyn/core.py b/dyn/core.py index 53508e9..7a2e378 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -6,10 +6,11 @@ """ import base64 import copy -import time import locale import logging +import re import threading +import time from datetime import datetime from . import __version__ @@ -114,6 +115,7 @@ def __init__(self, host=None, port=443, ssl=True, history=False, self._encoding = locale.getdefaultlocale()[-1] or 'UTF-8' self._token = self._conn = self._last_response = None self._permissions = None + self._tasks = {} @classmethod def new_session(cls, *args, **kwargs): @@ -183,9 +185,9 @@ def connect(self): use_proxy = True if self.proxy_user and self.proxy_pass: - auth = '{}:{}'.format(self.proxy_user, self.proxy_pass) - headers['Proxy-Authorization'] = 'Basic ' + base64.b64encode( - auth) + auth = '{}:{}'.format(self.proxy_user, self.proxy_pass).encode() + headers['Proxy-Authorization'] = 'Basic ' + str(base64.b64encode( + auth)) if use_proxy: if self.ssl: @@ -243,6 +245,39 @@ def _handle_error(self, uri, method, raw_args): """ return None + def _retry(self, msgs, final=False): + """Retry logic around throttled or blocked tasks""" + + throttle_err = 'RATE_LIMIT_EXCEEDED' + throttled = any(throttle_err == err['ERR_CD'] for err in msgs) + + if throttled: + # We're rate limited, so wait 5 seconds and try again + return dict(retry=True, wait=5, final=final) + + blocked_err = 'Operation blocked by current task' + blocked = any(blocked_err in err['INFO'] for err in msgs) + + pat = re.compile(r'^task_id:\s+(\d+)$') + if blocked: + try: + # Get the task id + task = next(pat.match(i['INFO']).group(1) for i in msgs + if pat.match(i.get('INFO', ''))) + except: + # Task id could not be recovered + wait = 1 + else: + # Exponential backoff for individual blocked tasks + wait = self._tasks.get(task, 1) + self._tasks[task] = wait * 2 + 1 + + # Give up if final or wait > 30 seconds + return dict(retry=True, wait=wait, final=wait > 30 or final) + + # Neither blocked nor throttled? + return dict(retry=False, wait=0, final=True) + def _handle_response(self, response, uri, method, raw_args, final): """Handle the processing of the API's response""" body = response.read() @@ -252,18 +287,34 @@ def _handle_response(self, response, uri, method, raw_args, final): if self.poll_incomplete: response, body = self.poll_response(response, body) self._last_response = response - ret_val = json.loads(body.decode('UTF-8')) + + if not body: + err_msg_fmt = "Received Empty Response: {!r} status: {!r} {!r}" + error_message = err_msg_fmt.format(body, response.status, uri) + self.logger.error(error_message) + raise ValueError(error_message) + + json_err_fmt = "Decode Error on Response Body: {!r} status: {!r} {!r}" + try: + ret_val = json.loads(body.decode('UTF-8')) + except ValueError: + self.logger.error(json_err_fmt.format(body, response.status, uri)) + raise + if self.__call_cache is not None: self.__call_cache.append((uri, method, clean_args(raw_args), ret_val['status'])) self._meta_update(uri, method, ret_val) - # Handle retrying if ZoneProp is blocking the current task - error_msg = 'Operation blocked by current task' - if ret_val['status'] == 'failure' and error_msg in \ - ret_val['msgs'][0]['INFO'] and not final: - time.sleep(8) - return self.execute(uri, method, raw_args, final=True) + + retry = {} + # Try to retry? + if ret_val['status'] == 'failure' and not final: + retry = self._retry(ret_val['msgs'], final) + + if retry.get('retry', False): + time.sleep(retry['wait']) + return self.execute(uri, method, raw_args, final=retry['final']) else: return self._process_response(ret_val, method) diff --git a/dyn/encrypt.py b/dyn/encrypt.py index 55d4130..e0257b2 100644 --- a/dyn/encrypt.py +++ b/dyn/encrypt.py @@ -29,6 +29,8 @@ def generate_key(force=False): key = ''.join([random.SystemRandom().choice(choices) for i in range(50)]) generate_key.secret_key = key return generate_key.secret_key + + generate_key.secret_key = None @@ -55,6 +57,7 @@ def encrypt(self, raw): :param raw: The raw password string to encode """ raw = self._pad(raw) + Random.atfork() iv = Random.new().read(AES.block_size) cipher = AES.new(self.key, AES.MODE_CBC, iv) return base64.b64encode(iv + cipher.encrypt(raw)) diff --git a/dyn/tm/errors.py b/dyn/tm/errors.py index cedd392..b434f60 100644 --- a/dyn/tm/errors.py +++ b/dyn/tm/errors.py @@ -142,6 +142,7 @@ def __repr__(self): def __str__(self): return self.message + ACTION_ERRORS = (DynectAuthError, DynectCreateError, DynectUpdateError, DynectGetError, DynectDeleteError) diff --git a/dyn/tm/records.py b/dyn/tm/records.py index 4ac1888..b23ca4a 100644 --- a/dyn/tm/records.py +++ b/dyn/tm/records.py @@ -9,11 +9,11 @@ from ..compat import force_unicode __author__ = 'jnappi' -__all__ = ['DNSRecord', 'ARecord', 'AAAARecord', 'ALIASRecord', 'CDSRecord', - 'CDNSKEYRecord', 'CERTRecord', 'CNAMERecord', 'CSYNCRecord', - 'DHCIDRecord', 'DNAMERecord', 'DNSKEYRecord', 'DSRecord', - 'KEYRecord', 'KXRecord', 'LOCRecord', 'IPSECKEYRecord', 'MXRecord', - 'NAPTRRecord', 'PTRRecord', 'PXRecord', 'NSAPRecord', +__all__ = ['DNSRecord', 'ARecord', 'AAAARecord', 'ALIASRecord', 'CAARecord', + 'CDSRecord', 'CDNSKEYRecord', 'CERTRecord', 'CNAMERecord', + 'CSYNCRecord', 'DHCIDRecord', 'DNAMERecord', 'DNSKEYRecord', + 'DSRecord', 'KEYRecord', 'KXRecord', 'LOCRecord', 'IPSECKEYRecord', + 'MXRecord', 'NAPTRRecord', 'PTRRecord', 'PXRecord', 'NSAPRecord', 'RPRecord', 'NSRecord', 'SOARecord', 'SPFRecord', 'SRVRecord', 'TLSARecord', 'TXTRecord', 'SSHFPRecord', 'UNKNOWNRecord'] @@ -1240,6 +1240,116 @@ def __repr__(self): return self.__str__() +class CAARecord(DNSRecord): + """Certification Authority Authorization (CAA) Resource Record + + This record allows a DNS domain name holder to specify one or more + Certification Authorities (CAs) authorized to issue certificates for that + domain. CAA Resource Records allow a public Certification Authority to + implement additional controls to reduce the risk of unintended certificate + mis-issue. This document defines the syntax of the CAA record and rules + for processing CAA records by certificate issuers. + + see: https://tools.ietf.org/html/rfc6844 """ + + def __init__(self, zone, fqdn, *args, **kwargs): + """Create a :class:`~dyn.tm.records.CAARecord` object + + :param zone: Name of zone where the record will be added + :param fqdn: Name of node where the record will be added + :param flags: A byte + :param tag: A string defining the tag component of the = + record property. May be one of: + issue: The issue property entry authorizes the holder of + the domain name or a party acting under + the explicit authority of the holder of that domain name to + issue certificates for the domain in which the property is + published. + + issuewild: The issuewild property entry authorizes the + holder of the domain name or a party + acting under the explicit authority of the holder of that + domain name to issue wildcard certificates for the domain in + which the property is published. + + iodef: Specifies a URL to which an issuer MAY report + certificate issue requests that are inconsistent with the + issuer's Certification Practices or Certificate Policy, or + that a Certificate Evaluator may use to report observation + of a possible policy violation. + :param value: A string representing the value component of the + property. This will be an issuer domain name or a URL. + :param ttl: TTL for this record. Use 0 for zone default + """ + fields = ['flags', 'tag', 'value', 'ttl'] + + create = kwargs.pop('create', None) + if create is not None: + super(CAARecord, self).__init__(zone, fqdn, create) + self._build(kwargs) + self._record_type = 'CAARecord' + else: + super(CAARecord, self).__init__(zone, fqdn) + self._record_type = 'CAARecord' + arg_length = len(args) + len(kwargs) + if 'record_id' in kwargs: + self._get_record(kwargs['record_id']) + elif arg_length == 1: + self._get_record(*args, **kwargs) + elif any(field in kwargs for field in fields) or arg_length >= 1: + self._post(*args, **kwargs) + + def _post(self, flags, tag, value, ttl=0): + self._flags = flags + self._tag = tag + self._value = value + self._ttl = ttl + self.api_args = dict( + rdata=dict(flags=flags, tag=tag, value=value), + ttl=ttl + ) + self._create_record(self.api_args) + + def rdata(self): + return dict(caa_rdata=super(CAARecord, self).rdata()) + + @property + def flags(self): + self._pull() + return self._flags + + @flags.setter + def flags(self, value): + self.api_args['rdata']['flags'] = value + self._update_record(self.api_args) + if self._implicitPublish: + self._flags = value + + @property + def tag(self): + self._pull() + return self._tag + + @tag.setter + def tag(self, value): + self.api_args['rdata']['tag'] = value + self._update_record(self.api_args) + if self._implicitPublish: + self._tag = value + + @property + def value(self): + self._pull() + return self._value + + @value.setter + def value(self, value): + self.api_args['rdata']['value'] = value + self._update_record(self.api_args) + if self._implicitPublish: + self._value = value + + class DSRecord(DNSRecord): """The Delegation Signer (DS) record type is used in DNSSEC to create the chain of trust or authority from a signed parent zone to a signed child diff --git a/dyn/tm/services/dnssec.py b/dyn/tm/services/dnssec.py index 3f3dd80..c911bd9 100644 --- a/dyn/tm/services/dnssec.py +++ b/dyn/tm/services/dnssec.py @@ -30,13 +30,20 @@ def __init__(self, key_type, algorithm, bits, start_ts=None, lifetime=None, """Create a :class:`DNSSECKey` object :param key_type: The type of this key. (KSK or ZSK) - :param algorithm: One of (RSA/SHA-1, RSA/SHA-256, RSA/SHA-512, DSA) - :param bits: length of the key. Valid values: 1024, 2048, or 4096 + :param algorithm: One of (RSA/SHA-1, RSA/SHA-256, RSA/SHA-512, DSA, + ECDSAP256SHA256, ECDSAP384SHA384) + :param bits: length of the key. Valid values: 256, 384, 1024, 2048, + or 4096 :param start_ts: An epoch time when key is to be valid :param lifetime: Lifetime of the key expressed in seconds :param overlap: Time before key expiration when a replacement key is prepared, expressed in seconds. Default = 7 days. :param expire_ts: An epoch time when this key is to expire + :param dnskey: The KSK or ZSK record data + :param ds: One of the DS records for the KSK. ZSKs will have this + value intialized, but with null values. + :param all_ds: All the DS records associated with this KSK. Applies + only to KSK, ZSK will have a zero-length list. """ super(DNSSECKey, self).__init__() self.key_type = key_type @@ -48,7 +55,7 @@ def __init__(self, key_type, algorithm, bits, start_ts=None, lifetime=None, self.lifetime = lifetime self.overlap = overlap self.expire_ts = expire_ts - self.dnssec_key_id = self.dnskey = self.ds = None + self.dnssec_key_id = self.dnskey = self.ds = self.all_ds = None for key, val in kwargs.items(): setattr(self, key, val) diff --git a/dyn/tm/services/dsf.py b/dyn/tm/services/dsf.py index df59591..3801a21 100644 --- a/dyn/tm/services/dsf.py +++ b/dyn/tm/services/dsf.py @@ -2,12 +2,15 @@ """This module contains wrappers for interfacing with every element of a Traffic Director (DSF) service. """ -from collections import Iterable +try: + from collections.abc import Iterable +except ImportError: + from collections import Iterable from dyn.compat import force_unicode, string_types from dyn.tm.utils import APIList, Active from dyn.tm.errors import DynectInvalidArgumentError -from dyn.tm.records import (ARecord, AAAARecord, ALIASRecord, CDSRecord, - CDNSKEYRecord, CSYNCRecord, CERTRecord, +from dyn.tm.records import (ARecord, AAAARecord, ALIASRecord, CAARecord, + CDSRecord, CDNSKEYRecord, CSYNCRecord, CERTRecord, CNAMERecord, DHCIDRecord, DNAMERecord, DNSKEYRecord, DSRecord, KEYRecord, KXRecord, LOCRecord, IPSECKEYRecord, MXRecord, NAPTRRecord, @@ -633,11 +636,15 @@ def implicit_publish(self, value): implicitPublish = implicit_publish # NOQA - def delete(self, notes=None): + def delete(self, notes=None, publish=True): """Delete this :class:`DSFRecord` :param notes: Optional zone publish notes + :param publish: Publish at run time. Default is True """ - api_args = {'publish': 'Y'} + api_args = {} + + if publish and self._implicitPublish: + api_args['publish'] = 'Y' if notes: api_args['notes'] = notes uri = '/DSFRecord/{}/{}'.format(self._service_id, self._dsf_record_id) @@ -2048,11 +2055,15 @@ def to_json(self, svc_id=None, skip_svc=False): json_blob['records'] = [] return json_blob - def delete(self, notes=None): + def delete(self, notes=None, publish=True): """Delete this :class:`DSFRecordSet` from the Dynect System :param notes: Optional zone publish notes + :param publish: Publish at run time. Default is True """ - api_args = {'publish': 'Y'} + api_args = {} + + if publish and self._implicitPublish: + api_args['publish'] = 'Y' if notes: api_args['notes'] = notes DynectSession.get_session().execute(self.uri, 'DELETE', api_args) @@ -2342,11 +2353,15 @@ def implicit_publish(self, value): implicitPublish = implicit_publish - def delete(self, notes=None): + def delete(self, notes=None, publish=True): """Delete this :class:`DSFFailoverChain` from the Dynect System :param notes: Optional zone publish notes + :param publish: Publish at run time. Default is True """ - api_args = {'publish': 'Y'} + api_args = {} + + if publish and self._implicitPublish: + api_args['publish'] = 'Y' if notes: api_args['notes'] = notes DynectSession.get_session().execute(self.uri, 'DELETE', api_args) @@ -2651,11 +2666,15 @@ def implicit_publish(self, value): implicitPublish = implicit_publish - def delete(self, notes=None): + def delete(self, notes=None, publish=True): """Delete this :class:`DSFResponsePool` from the DynECT System :param notes: Optional zone publish notes + :param publish: Publish at run time. Default is True """ - api_args = {'publish': 'Y'} + api_args = {} + + if publish and self._implicitPublish: + api_args['publish'] = 'Y' if notes: api_args['notes'] = notes DynectSession.get_session().execute(self.uri, 'DELETE', api_args) @@ -3039,12 +3058,16 @@ def _json(self, svc_id=None, skip_svc=False): return json_blob - def delete(self, notes=None): + def delete(self, notes=None, publish=True): """Remove this :class:`DSFRuleset` from it's associated :class:`TrafficDirector` Service :param notes: Optional zone publish notes + :param publish: Publish at run time. Default is True """ - api_args = {'publish': 'Y'} + api_args = {} + + if publish and self._implicitPublish: + api_args['publish'] = 'Y' if notes: api_args['notes'] = notes DynectSession.get_session().execute(self.uri, 'DELETE', api_args) @@ -3079,7 +3102,7 @@ def _update(self, api_args): full_list = self._monitor.endpoints args_list = [] for endpoint in full_list: - if id(endpoint) == id(self): + if endpoint.address == self.address: args_list.append(api_args) else: args_list.append(endpoint._json) @@ -3558,7 +3581,7 @@ def __init__(self, zone, fqdn=None): self.records = {} self.recs = {'A': ARecord, 'AAAA': AAAARecord, - 'ALIAS': ALIASRecord, 'CDS': CDSRecord, + 'ALIAS': ALIASRecord, 'CAA': CAARecord, 'CDS': CDSRecord, 'CDNSKEY': CDNSKEYRecord, 'CSYNC': CSYNCRecord, 'CERT': CERTRecord, 'CNAME': CNAMERecord, 'DHCID': DHCIDRecord, 'DNAME': DNAMERecord, @@ -3577,10 +3600,10 @@ def add_record(self, record_type='A', *args, **kwargs): """Adds an a record with the provided data to this :class:`Node` :param record_type: The type of record you would like to add. - Valid record_type arguments are: 'A', 'AAAA', 'CERT', 'CNAME', - 'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC', 'IPSECKEY', - 'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA', 'SPF', - 'SRV', and 'TXT'. + Valid record_type arguments are: 'A', 'AAAA', 'CAA', 'CERT', + 'CNAME', 'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC', + 'IPSECKEY', 'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA', + 'SPF', 'SRV', and 'TXT'. :param args: Non-keyword arguments to pass to the Record constructor :param kwargs: Keyword arguments to pass to the Record constructor """ @@ -3630,13 +3653,14 @@ def get_all_records_by_type(self, record_type): are owned by this node. :param record_type: The type of :class:`DNSRecord` you wish returned. - Valid record_type arguments are: 'A', 'AAAA', 'CERT', 'CNAME', - 'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC', 'IPSECKEY', - 'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA', 'SPF', - 'SRV', and 'TXT'. + Valid record_type arguments are: 'A', 'AAAA', 'CAA', 'CERT', + 'CNAME', 'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC', + 'IPSECKEY', 'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA', + 'SPF', 'SRV', and 'TXT'. :return: A list of :class:`DNSRecord`'s """ - names = {'A': 'ARecord', 'AAAA': 'AAAARecord', 'CERT': 'CERTRecord', + names = {'A': 'ARecord', 'AAAA': 'AAAARecord', + 'CAA': 'CAARecord', 'CERT': 'CERTRecord', 'CNAME': 'CNAMERecord', 'DHCID': 'DHCIDRecord', 'DNAME': 'DNAMERecord', 'DNSKEY': 'DNSKEYRecord', 'DS': 'DSRecord', 'KEY': 'KEYRecord', 'KX': 'KXRecord', diff --git a/dyn/tm/zones.py b/dyn/tm/zones.py index d4cf626..a8b57be 100644 --- a/dyn/tm/zones.py +++ b/dyn/tm/zones.py @@ -9,7 +9,7 @@ from dyn.tm.errors import (DynectCreateError, DynectGetError, DynectInvalidArgumentError) from dyn.tm.records import (ARecord, AAAARecord, ALIASRecord, CDSRecord, - CDNSKEYRecord, CSYNCRecord, CERTRecord, + CAARecord, CDNSKEYRecord, CSYNCRecord, CERTRecord, CNAMERecord, DHCIDRecord, DNAMERecord, DNSKEYRecord, DSRecord, KEYRecord, KXRecord, LOCRecord, IPSECKEYRecord, MXRecord, NAPTRRecord, @@ -23,18 +23,19 @@ from dyn.tm.task import Task __author__ = 'jnappi' -__all__ = ['get_all_zones', 'Zone', 'SecondaryZone', 'Node'] +__all__ = ['get_all_zones', 'Zone', 'SecondaryZone', 'Node', + 'ExternalNameserver', 'ExternalNameserverEntry'] RECS = {'A': ARecord, 'AAAA': AAAARecord, 'ALIAS': ALIASRecord, - 'CDS': CDSRecord, 'CDNSKEY': CDNSKEYRecord, 'CSYNC': CSYNCRecord, - 'CERT': CERTRecord, 'CNAME': CNAMERecord, 'DHCID': DHCIDRecord, - 'DNAME': DNAMERecord, 'DNSKEY': DNSKEYRecord, 'DS': DSRecord, - 'KEY': KEYRecord, 'KX': KXRecord, 'LOC': LOCRecord, + 'CAA': CAARecord, 'CDS': CDSRecord, 'CDNSKEY': CDNSKEYRecord, + 'CSYNC': CSYNCRecord, 'CERT': CERTRecord, 'CNAME': CNAMERecord, + 'DHCID': DHCIDRecord, 'DNAME': DNAMERecord, 'DNSKEY': DNSKEYRecord, + 'DS': DSRecord, 'KEY': KEYRecord, 'KX': KXRecord, 'LOC': LOCRecord, 'IPSECKEY': IPSECKEYRecord, 'MX': MXRecord, 'NAPTR': NAPTRRecord, 'PTR': PTRRecord, 'PX': PXRecord, 'NSAP': NSAPRecord, - 'RP': RPRecord, 'NS': NSRecord, 'SOA': SOARecord, - 'SPF': SPFRecord, 'SRV': SRVRecord, 'TLSA': TLSARecord, - 'TXT': TXTRecord, 'SSHFP': SSHFPRecord, 'UNKNOWN': UNKNOWNRecord} + 'RP': RPRecord, 'NS': NSRecord, 'SOA': SOARecord, 'SPF': SPFRecord, + 'SRV': SRVRecord, 'TLSA': TLSARecord, 'TXT': TXTRecord, + 'SSHFP': SSHFPRecord, 'UNKNOWN': UNKNOWNRecord} def get_all_zones(): @@ -521,24 +522,23 @@ def get_all_records_by_type(self, record_type): are owned by this node. :param record_type: The type of :class:`DNSRecord` you wish returned. - Valid record_type arguments are: 'A', 'AAAA', 'CERT', 'CNAME', - 'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC', 'IPSECKEY', - 'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA', 'SPF', - 'SRV', and 'TXT'. + Valid record_type arguments are: 'A', 'AAAA', 'CAA', 'CERT', + 'CNAME', 'DHCID', 'DNAME', 'DNSKEY', 'DS', 'KEY', 'KX', 'LOC', + 'IPSECKEY', 'MX', 'NAPTR', 'PTR', 'PX', 'NSAP', 'RP', 'NS', 'SOA', + 'SPF', 'SRV', and 'TXT'. :return: A :class:`List` of :class:`DNSRecord`'s """ names = {'A': 'ARecord', 'AAAA': 'AAAARecord', 'ALIAS': 'ALIASRecord', - 'CDS': 'CDSRecord', 'CDNSKEY': 'CDNSKEYRecord', - 'CERT': 'CERTRecord', 'CSYNC': 'CSYNCRecord', - 'CNAME': 'CNAMERecord', 'DHCID': 'DHCIDRecord', - 'DNAME': 'DNAMERecord', 'DNSKEY': 'DNSKEYRecord', - 'DS': 'DSRecord', 'KEY': 'KEYRecord', 'KX': 'KXRecord', - 'LOC': 'LOCRecord', 'IPSECKEY': 'IPSECKEYRecord', - 'MX': 'MXRecord', 'NAPTR': 'NAPTRRecord', 'PTR': 'PTRRecord', - 'PX': 'PXRecord', 'NSAP': 'NSAPRecord', 'RP': 'RPRecord', - 'NS': 'NSRecord', 'SOA': 'SOARecord', 'SPF': 'SPFRecord', - 'SRV': 'SRVRecord', 'TLSA': 'TLSARecord', 'TXT': 'TXTRecord', - 'SSHFP': 'SSHFPRecord'} + 'CAA': 'CAARecord', 'CDS': 'CDSRecord', 'CDNSKEY': + 'CDNSKEYRecord', 'CERT': 'CERTRecord', 'CSYNC': 'CSYNCRecord', + 'CNAME': 'CNAMERecord', 'DHCID': 'DHCIDRecord', 'DNAME': + 'DNAMERecord', 'DNSKEY': 'DNSKEYRecord', 'DS': 'DSRecord', + 'KEY': 'KEYRecord', 'KX': 'KXRecord', 'LOC': 'LOCRecord', + 'IPSECKEY': 'IPSECKEYRecord', 'MX': 'MXRecord', 'NAPTR': + 'NAPTRRecord', 'PTR': 'PTRRecord', 'PX': 'PXRecord', 'NSAP': + 'NSAPRecord', 'RP': 'RPRecord', 'NS': 'NSRecord', 'SOA': + 'SOARecord', 'SPF': 'SPFRecord', 'SRV': 'SRVRecord', 'TLSA': + 'TLSARecord', 'TXT': 'TXTRecord', 'SSHFP': 'SSHFPRecord'} constructor = RECS[record_type] uri = '/{}/{}/{}/'.format(names[record_type], self._name, self.fqdn) @@ -634,10 +634,11 @@ def get_all_httpredirect(self): response = DynectSession.get_session().execute(uri, 'GET', api_args) httpredirs = [] for httpredir in response['data']: + fqdn = httpredir['fqdn'] del httpredir['zone'] del httpredir['fqdn'] httpredirs.append( - HTTPRedirect(self._name, self._fqdn, api=False, **httpredir)) + HTTPRedirect(self._name, fqdn, api=False, **httpredir)) return httpredirs def get_all_advanced_redirect(self): @@ -838,7 +839,8 @@ def _build(self, data): @property def task(self): - """:class:`Task` for most recent system action on this :class:`SecondaryZone`. + """:class:`Task` for most recent system action + on this :class:`SecondaryZone`. """ if self._task_id: self._task_id.refresh() @@ -1050,16 +1052,18 @@ def get_all_records_by_type(self, record_type): 'SRV', and 'TXT'. :return: A list of :class:`DNSRecord`'s """ - names = {'A': 'ARecord', 'AAAA': 'AAAARecord', 'CERT': 'CERTRecord', - 'CNAME': 'CNAMERecord', 'DHCID': 'DHCIDRecord', - 'DNAME': 'DNAMERecord', 'DNSKEY': 'DNSKEYRecord', - 'DS': 'DSRecord', 'KEY': 'KEYRecord', 'KX': 'KXRecord', - 'LOC': 'LOCRecord', 'IPSECKEY': 'IPSECKEYRecord', - 'MX': 'MXRecord', 'NAPTR': 'NAPTRRecord', 'PTR': 'PTRRecord', + names = {'A': 'ARecord', 'AAAA': 'AAAARecord', 'CAA': 'CAARecord', + 'CERT': 'CERTRecord', 'CNAME': 'CNAMERecord', + 'DHCID': 'DHCIDRecord', 'DNAME': 'DNAMERecord', + 'DNSKEY': 'DNSKEYRecord', 'DS': 'DSRecord', + 'KEY': 'KEYRecord', 'KX': 'KXRecord', 'LOC': 'LOCRecord', + 'IPSECKEY': 'IPSECKEYRecord', 'MX': 'MXRecord', + 'NAPTR': 'NAPTRRecord', 'PTR': 'PTRRecord', 'PX': 'PXRecord', 'NSAP': 'NSAPRecord', 'RP': 'RPRecord', 'NS': 'NSRecord', 'SOA': 'SOARecord', 'SPF': 'SPFRecord', - 'SRV': 'SRVRecord', 'TLSA': 'TLSARecord', 'TXT': 'TXTRecord', - 'SSHFP': 'SSHFPRecord', 'ALIAS': 'ALIASRecord'} + 'SRV': 'SRVRecord', 'TLSA': 'TLSARecord', + 'TXT': 'TXTRecord', 'SSHFP': 'SSHFPRecord', + 'ALIAS': 'ALIASRecord'} constructor = RECS[record_type] uri = '/{}/{}/{}/'.format(names[record_type], self.zone, self.fqdn) @@ -1210,3 +1214,218 @@ def delete(self): api_args = {} DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + + +class ExternalNameserver(object): + """A class representing DynECT External Nameserver """ + + def __init__(self, zone, *args, **kwargs): + """Create a :class:`ExternalNameserver` object + + :param zone: The name of the zone for this :class:`ExternalNameserver` + :param deny: does this block requests or add them + :param hosts: list of :class:`ExternalNameserverEntry` + :param active: active? Y/N + :param tsig_key_name: Name of TSIG to associate with this + :class:`ExternalNameserver` + + """ + self._zone = zone + self.uri = '/ExtNameserver/{}/'.format(self._zone) + self._deny = None + self._hosts = None + self._active = None + self._tsig_key_name = None + + if len(args) == 0 and len(kwargs) == 0: + self._get() + else: + self._post(*args, **kwargs) + + def _get(self): + """Get a :class:`ExternalNameserver` object from the DynECT System""" + api_args = {'zone': self._zone} + response = DynectSession.get_session().execute(self.uri, 'GET', + api_args) + self._build(response['data']) + + def _post(self, *args, **kwargs): + """Create a new :class:`ExternalNameserver` + object on the DynECT System""" + api_args = {'zone': self._zone} + self._deny = kwargs.get('deny', None) + if self._deny: + api_args['deny'] = self._deny + + self._tsig_key_name = kwargs.get('tsig_key_name', None) + if self._tsig_key_name: + api_args['tsig_key_name'] = self._tsig_key_name + + self._active = kwargs.get('active', None) + if self._active: + api_args['active'] = self._active + + self._hosts = kwargs.get('hosts', None) + if self._hosts: + api_args['hosts'] = list() + for host in self._hosts: + if isinstance(host, ExternalNameserverEntry): + api_args['hosts'].append(host._json) + else: + api_args['hosts'].append(host) + response = DynectSession.get_session().execute(self.uri, 'POST', + api_args) + self._build(response['data']) + + def _update(self, api_args=None): + """Update an existing :class:`AdvancedRedirect` Service + on the DynECT System""" + response = DynectSession.get_session().execute(self.uri, 'PUT', + api_args) + self._build(response['data']) + + def _build(self, data): + self._hosts = [] + for key, val in data.items(): + if key == 'hosts': + for host in val: + host['api'] = 'Y' + self._hosts.append(ExternalNameserverEntry( + host['address'], notifies=host['notifies'])) + continue + setattr(self, '_' + key, val) + + @property + def deny(self): + """Gets deny value :class:`ExternalNameserver` object""" + self._get() + return self._deny + + @deny.setter + def deny(self, deny): + """ + Sets deny value of :class:`ExternalNameserver` object + :param deny: Y/N + """ + api_args = {'zone': self._zone, 'deny': deny} + self._update(api_args=api_args) + + @property + def tsig_key_name(self): + """Gets tsig_key_name value :class:`ExternalNameserver` object""" + self._get() + return self._tsig_key_name + + @tsig_key_name.setter + def tsig_key_name(self, tsig_key_name): + """ + Sets deny value of :class:`ExternalNameserver` object + :param deny: Y/N + """ + api_args = {'zone': self._zone, 'tsig_key_name': tsig_key_name} + self._update(api_args=api_args) + + @property + def hosts(self): + """ + :class:`ExternalNameserver` hosts. list of ExternalNameserverEntries + """ + self._get() + return self._hosts + + @hosts.setter + def hosts(self, value): + api_args = dict() + api_args['hosts'] = list() + for host in value: + if isinstance(host, ExternalNameserverEntry): + api_args['hosts'].append(host._json) + else: + api_args['hosts'].append(host) + self._update(api_args) + + @property + def active(self): + """Gets active status of :class:`ExternalNameserver` object. """ + self._get() + return self._active + + @active.setter + def active(self, active): + """ + Sets active status of :class:`ExternalNameserver` object. + :param active: Y/N + """ + api_args = {'zone': self._zone, 'active': active} + self._update(api_args=api_args) + + @property + def zone(self): + """Gets name of zone in :class:`ExternalNameserver`""" + return self._zone + + def delete(self): + api_args = {} + DynectSession.get_session().execute(self.uri, 'DELETE', + api_args) + + +class ExternalNameserverEntry(object): + """A class representing DynECT :class:`ExternalNameserverEntry`""" + + def __init__(self, address, *args, **kwargs): + """Create a :class:`ExternalNameserverEntry` object + + :param address: address or CIDR of this nameserver Entry + :param notifies: Y/N Do we send notifies to this host? + + """ + self._address = address + self._notifies = kwargs.get('notifies') + + @property + def _json(self): + """Get the JSON representation of this :class:`ExternalNameserverEntry` + object + """ + json_blob = {'address': self._address, 'notifies': self._notifies} + return {k: v for k, v in json_blob.items() if v is not None} + + @property + def address(self): + """Gets address value :class:`ExternalNameserverEntry` object""" + return self._address + + @address.setter + def address(self, address): + """ + Sets address of :class:`ExternalNameserverEntry` object + :param address: address or CIDR + """ + self._address = address + + @property + def notifies(self): + """Gets address value :class:`ExternalNameserverEntry` object""" + return self._notifies + + @notifies.setter + def notifies(self, notifies): + """ + Sets notifies of :class:`ExternalNameserverEntry` object + :param notifies: send notifies to this server. Y/N + """ + self._notifies = notifies + + def __str__(self): + """str override""" + return force_unicode( + ': {}, Notifies: {}').format( + self._address, + self._notifies) + + __repr__ = __unicode__ = __str__ + + def __bytes__(self): + """bytes override""" + return bytes(self.__str__())