From 579dbce196a6d987c531b8c837fecfa6a22644e4 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Tue, 11 Nov 2014 15:50:57 -0500 Subject: [PATCH 01/59] First pass at implementing an API interface for the objects in this framework --- dyn/core.py | 171 +++++++++++++- dyn/tm/accounts.py | 551 +++++++-------------------------------------- dyn/tm/zones.py | 81 ++++--- 3 files changed, 289 insertions(+), 514 deletions(-) diff --git a/dyn/core.py b/dyn/core.py index e27d62e..1b75691 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """dyn.core is a utilities module for use internally within the dyn library itself. Although it's possible to use this functionality outside of the dyn -library, it is not recommened and could possible result in some strange +library, it is not recommened and could possibly result in some strange behavior. """ import copy @@ -10,10 +10,11 @@ import logging import threading from datetime import datetime +from collections import OrderedDict from . import __version__ from .compat import (HTTPConnection, HTTPSConnection, HTTPException, json, - prepare_to_send, force_unicode) + prepare_to_send, force_unicode, string_types) def cleared_class_dict(dict_obj): @@ -38,6 +39,7 @@ def clean_args(dict_obj): class _Singleton(type): _instances = {} + def __call__(cls, *args, **kwargs): cur_thread = threading.current_thread() key = getattr(cls, '__metakey__') @@ -61,10 +63,155 @@ class Singleton(_Singleton('SingletonMeta', (object,), {})): pass +class APIDescriptor(object): + def __init__(self, name='', doc=None): + super(APIDescriptor, self).__init__() + self.name = name + self.private_name = '_' + self.name + self.__doc__ = doc + + def __get__(self, instance, cls): + print('GET', instance, cls) + return getattr(instance, self.private_name, None) + + def __set__(self, instance, value): + print('SET', instance, value) + setattr(instance, self.private_name, value) + if hasattr(instance, '_update'): + args = {self.name: value} + getattr(instance, '_update')(args) + + +class Typed(APIDescriptor): + """Type enforced descriptor""" + ty = object + + def __set__(self, instance, value): + if not isinstance(value, self.ty): + raise TypeError('Expected %s' % self.ty) + super(Typed, self).__set__(instance, value) + + +class IntegerField(Typed): + """API Field that may only be an integer type""" + ty = int + + +class StringField(Typed): + """API Field that may only be a string type""" + ty = string_types + + +class ImmutableField(APIDescriptor): + """An API field that can not be overridden""" + + def __set__(self, instance, cls): + pass + + +class ValidatedField(APIDescriptor): + """An API field whose value can be forced to a specific subset of values""" + def __init__(self, name='', doc=None, validator=None): + """An API field that must be one of a specific set of values + + :param name: The name of this field + :param validator: An optional list of valid values for this field + """ + super(ValidatedField, self).__init__(name, doc) + self.validator = validator + + def __set__(self, instance, value): + if self.validator is not None: + if value in self.validator: + super(ValidatedField, self).__set__(instance, value) + else: # If we have no validator, then assume it's safe to overwrite + super(ValidatedField, self).__set__(instance, value) + + +# class Junk(object): +# data = APIDescriptor('data') +# odds = ValidatedField('odds', validator=[1, 3, 5, 7]) +# read_only = ImmutableField('read_only') +# my_int = IntegerField('my_int') +# my_str = StringField('my_str') +# +# def __init__(self): +# self._data = {'serial': 121312, 'ts': 12321111233} +# self._odds = 5 +# self._read_only = 'passw0rd' +# self._my_int = 0 +# self._my_str = 'lulz' + + +# noinspection PyUnusedLocal +class APIObject(object): + """Base API Object type responsible for handling shared functionality + between various objects constructed from what comes out of the API. This + base object has some safe default behavior such as a _build method that + simply takes in api data and appends a '_' to the name before assigning it + as a variable for the instance. A _get method that passes no arguments to + the API. And a delete that passes no arguments to the API. + + This class also provides a __json__ attribute that attempts to generify + the conversion of these Python objects into JSON blobs. By default this + property creates a cleared_class_dict and further filters out any instance + attributes that start with '_' but not '__'. + """ + uri = '' + session_type = None + + def __init__(self, *args, **kwargs): + super(APIObject, self).__init__() + if 'api' in kwargs: + del kwargs['api'] + self._build(kwargs) + elif len(args) == 0 and len(kwargs) == 0: # Safe default behaviour + self._get() + else: + self._post(*args, **kwargs) + + def _build(self, data): + """Build the variables in this object by pulling out the data from the + provided data dict + """ + for key, val in data.items(): + setattr(self, '_' + key, val) + + def _post(self, *args, **kwargs): + """POST method, to be inherited by all child classes on a case-by-case + basis + """ + raise NotImplementedError('_post must be overriden by child classes') + + def _get(self, *args, **kwargs): + """Retrieve this object from the DynECT System""" + response = self.session_type.get_session().execute(self.uri, 'GET') + self._build(response['data']) + + def _update(self, **api_args): + """Update this object on the DynECT System""" + response = self.session_type.get_session().execute(self.uri, 'PUT', + api_args) + self._build(response['data']) + + def delete(self, *args, **kwargs): + """Delete this object from the DynECT system""" + if self.session_type is not None: + self.session_type.get_session().execute(self.uri, 'DELETE') + + @property + def __json__(self): + """Generic JSON reprsentation of this object""" + dict_obj = cleared_class_dict(self.__dict__) + return {x: dict_obj[x] for x in dict_obj if x.startswith('_') and + not x.startswith('__')} + + class _History(list): """A *list* subclass specifically targeted at being able to store the history of calls made via a SessionEngine """ + def append(self, p_object): """Override builtin list append operators to allow for the automatic appendation of a timestamp for cleaner record keeping @@ -73,6 +220,7 @@ def append(self, p_object): super(_History, self).append(tuple([now_ts] + list(p_object))) +# noinspection PyMethodParameters class SessionEngine(Singleton): """Base object representing a DynectSession Session""" _valid_methods = tuple() @@ -164,9 +312,8 @@ def connect(self): self.logger.info(msg) self._conn = HTTPSConnection(self.host, self.port, timeout=300) else: - msg = \ - 'Establishing unencrypted connection to {}:{}'.format(self.host, - self.port) + msg = 'Establishing unencrypted connection to {}:{}' + msg = msg.format(self.host, self.port) self.logger.info(msg) self._conn = HTTPConnection(self.host, self.port, timeout=300) @@ -182,8 +329,8 @@ def _process_response(self, response, method, final=False): return response def _handle_error(self, uri, method, raw_args): - """Handle the processing of a connection error with the api. Note, to be - implemented as needed in subclasses. + """Handle the processing of a connection error with the api. Note, to + be implemented as needed in subclasses. """ return None @@ -267,7 +414,8 @@ def execute(self, uri, method, args=None, final=False): raw_args, args, uri = self._prepare_arguments(args, method, uri) msg = 'uri: {}, method: {}, args: {}' - self.logger.debug(msg.format(uri, method, clean_args(json.loads(args)))) + self.logger.debug(msg.format(uri, method, + clean_args(json.loads(args)))) # Send the command and deal with results self.send_command(uri, method, args) @@ -383,9 +531,9 @@ def __getstate__(cls): return d def __setstate__(cls, state): - """Because the HTTP/HTTPS connection was stripped out in __getstate__ we - must manually re-enter it as None and let the sessions execute method - handle rebuilding it later + """Because the HTTP/HTTPS connection was stripped out in __getstate__ + we must manually re-enter it as None and let the sessions execute + method handle rebuilding it later """ cls.__dict__ = state cls.__dict__['_conn'] = None @@ -393,6 +541,7 @@ def __setstate__(cls, state): def __str__(self): """str override""" return force_unicode('<{}>').format(self.name) + __repr__ = __unicode__ = __str__ def __bytes__(self): diff --git a/dyn/tm/accounts.py b/dyn/tm/accounts.py index 75503d5..1260b0e 100644 --- a/dyn/tm/accounts.py +++ b/dyn/tm/accounts.py @@ -2,10 +2,9 @@ """This module contains interfaces for all Account management features of the REST API """ -import logging - from .errors import DynectInvalidArgumentError from .session import DynectSession +from ..core import APIObject, ImmutableField, StringField, APIDescriptor from ..compat import force_unicode __author__ = 'jnappi' @@ -160,52 +159,25 @@ def get_notifiers(search=None): return notifiers -class UpdateUser(object): +class UpdateUser(APIObject): """:class:`~dyn.tm.accounts.UpdateUser` type objects are a special form of a :class:`~dyn.tm.accounts.User` which are tied to a specific Dynamic DNS services. """ - def __init__(self, *args, **kwargs): - """Create an :class:`~dyn.tm.accounts.UpdateUser` object - - :param user_name: the Username this - :class:`~dyn.tm.accounts.UpdateUser` uses or will use to log in to - the DynECT System. A :class:`~dyn.tm.accounts.UpdateUser`'s - `user_name` is required for both creating and getting - :class:`~dyn.tm.accounts.UpdateUser`'s. - :param nickname: When creating a new - :class:`~dyn.tm.accounts.UpdateUser` on the DynECT System, this - `nickname` will be the System nickname for this - :class:`~dyn.tm.accounts.UpdateUser` - :param password: When creating a new - :class:`~dyn.tm.accounts.UpdateUser` on the DynECT System, this - `password` will be the password this - :class:`~dyn.tm.accounts.UpdateUser` uses to log into the System - """ - super(UpdateUser, self).__init__() - self.uri = '/UpdateUser/' - self._password = self._status = self._user_name = self._nickname = None - if 'api' in kwargs: - good_args = ('user_name', 'status', 'password') - for key, val in kwargs.items(): - if key in good_args: - setattr(self, '_' + key, val) - self.uri = '/UpdateUser/{}/'.format(self._user_name) - elif len(args) + len(kwargs) == 1: - self._get(*args, **kwargs) - else: - self._post(*args, **kwargs) + uri = '/UpdateUser/' + session_type = DynectSession + user_name = ImmutableField('user_name') + password = StringField('password') + nickname = StringField('nickname') + status = ImmutableField('status') def _post(self, nickname, password): """Create a new :class:`~dyn.tm.accounts.UpdateUser` on the DynECT System """ - self._nickname = nickname - self._password = password - uri = '/UpdateUser/' - api_args = {'nickname': self._nickname, - 'password': self._password} - response = DynectSession.get_session().execute(uri, 'POST', api_args) + api_args = {'nickname': nickname, 'password': password} + response = DynectSession.get_session().execute(self.uri, 'POST', + api_args) self._build(response['data']) self.uri = '/UpdateUser/{}/'.format(self._user_name) @@ -218,85 +190,19 @@ def _get(self, user_name): response = DynectSession.get_session().execute(self.uri, 'GET') self._build(response['data']) - def _build(self, data): - for key, val in data.items(): - setattr(self, '_' + key, val) - - def _update(self, api_args=None): - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def user_name(self): - """This :class:`~dyn.tm.accounts.UpdateUser`'s `user_name`. An - :class:`~dyn.tm.accounts.UpdateUser`'s user_name is a read-only - property which can not be updated after the :class:`UpdateUser` has - been created. - """ - return self._user_name - @user_name.setter - def user_name(self, value): - pass - - @property - def nickname(self): - """This :class:`~dyn.tm.accounts.UpdateUser`s `nickname`. An - :class:`~dyn.tm.accounts.UpdateUser`'s `nickname` is a read-only - property which can not be updated after the - :class:`~dyn.tm.accounts.UpdateUser` has been created. - """ - return self._nickname - @nickname.setter - def nickname(self, value): - pass - - @property - def status(self): - """The current `status` of an :class:`~dyn.tm.accounts.UpdateUser` will - be one of either 'active' or 'blocked'. Blocked - :class:`~dyn.tm.accounts.UpdateUser`'s are unable to log into the - DynECT System, where active :class:`~dyn.tm.accounts.UpdateUser`'s are. - """ - return self._status - @status.setter - def status(self, value): - pass - - @property - def password(self): - """The current `password` for this - :class:`~dyn.tm.accounts.UpdateUser`. An - :class:`~dyn.tm.accounts.UpdateUser`'s `password` may be reassigned. - """ - if self._password is None or self._password == u'': - self._get(self._user_name) - return self._password - @password.setter - def password(self, new_password): - """Update this :class:`~dyn.tm.accounts.UpdateUser`'s password to be - the provided password - - :param new_password: The new password to use - """ - api_args = {'password': new_password} - self._update(api_args) - def block(self): """Set the status of this :class:`~dyn.tm.accounts.UpdateUser` to 'blocked'. This will prevent this :class:`~dyn.tm.accounts.UpdateUser` from logging in until they are explicitly unblocked. """ - api_args = {'block': True} - self._update(api_args) + self._update(block=True) def unblock(self): """Set the status of this :class:`~dyn.tm.accounts.UpdateUser` to 'active'. This will re-enable this :class:`~dyn.tm.accounts.UpdateUser` to be able to login if they were previously blocked. """ - api_args = {'unblock': True} - self._update(api_args) + self._update(unblock=True) def sync_password(self): """Pull in this :class:`~dyn.tm.accounts.UpdateUser` current password @@ -304,12 +210,11 @@ def sync_password(self): :class:`~dyn.tm.accounts.UpdateUser` object's password may have gotten out of sync """ - api_args = {'user_name': self._user_name} - self._update(api_args) + self._update(user_name=self._user_name) def delete(self): """Delete this :class:`~dyn.tm.accounts.UpdateUser` from the DynECT - System. It is important to note that this operation may not be undone. + System. It is important to note that this operation can not be undone. """ DynectSession.get_session().execute(self.uri, 'DELETE') @@ -323,8 +228,33 @@ def __bytes__(self): return bytes(self.__str__()) -class User(object): +# noinspection PyAttributeOutsideInit,PyUnresolvedReferences +class User(APIObject): """DynECT System User object""" + uri = '/UpdateUser/{user_name}/' + session_type = DynectSession + user_name = ImmutableField('user_name') + first_name = StringField('first_name') + last_name = StringField('last_name') + nickname = StringField('nickname') + password = StringField('password') + organization = StringField('organization') + phone = StringField('phone') + address = StringField('address') + address_2 = StringField('address_2') + city = StringField('city') + country = StringField('country') + fax = StringField('fax') + notify_email = StringField('notify_email') + pager_email = StringField('pager_email') + post_code = StringField('post_code') + group_name = StringField('group_name') + permision = StringField('permission') + zone = StringField('zone') + forbid = StringField('forbid') + website = StringField('website') + status = ImmutableField('status') + def __init__(self, user_name, *args, **kwargs): """Create a new :class:`~dyn.tm.accounts.User` object @@ -368,29 +298,11 @@ def __init__(self, user_name, *args, **kwargs): :param status: Current status of this :class:`~dyn.tm.accounts.User` :param website: This :class:`~dyn.tm.accounts.User`'s website """ - super(User, self).__init__() - self._user_name = user_name - self.uri = '/User/{}/'.format(self._user_name) - self._password = self._email = self._first_name = self._last_name = None - self._nickname = self._organization = self._phone = self._address = None - self._address_2 = self._city = self._country = self._fax = None - self._notify_email = self._pager_email = self._post_code = None - self._group_name = self._permission = self._zone = self._forbid = None - self._status = self._website = None + self.uri = self.uri.format(user_name=user_name) self.permissions = [] self.permission_groups = [] self.groups = [] - if 'api' in kwargs: - del kwargs['api'] - for key, val in kwargs.items(): - if key != '_user_name': - setattr(self, '_' + key, val) - else: - setattr(self, key, val) - elif len(args) == 0 and len(kwargs) == 0: - self._get() - else: - self._post(*args, **kwargs) + super(User, self).__init__(*args, **kwargs) def _post(self, password, email, first_name, last_name, nickname, organization, phone, address=None, address_2=None, city=None, @@ -422,257 +334,17 @@ def _post(self, password, email, first_name, last_name, nickname, self._status = status self._website = website response = DynectSession.get_session().execute(self.uri, 'POST', self) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - def _get(self): - """Get an existing :class:`~dyn.tm.accounts.User` object from the - DynECT System - """ - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - def _update(self, api_args=None): - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - @property - def user_name(self): - """A :class:`~dyn.tm.accounts.User`'s user_name is a read-only property - """ - return self._user_name - @user_name.setter - def user_name(self, value): - pass - - @property - def status(self): - """A :class:`~dyn.tm.accounts.User`'s status is a read-only property. - To change you must use the :meth:`block`/:meth:`unblock` methods - """ - return self._status - @status.setter - def status(self, value): - pass - - @property - def email(self): - """This :class:`~dyn.tm.accounts.User`'s Email address""" - return self._email - @email.setter - def email(self, value): - api_args = {'email': value} - self._update(api_args) - - @property - def first_name(self): - """This :class:`~dyn.tm.accounts.User`'s first name""" - return self._first_name - @first_name.setter - def first_name(self, value): - api_args = {'first_name': value} - self._update(api_args) - - @property - def last_name(self): - """This :class:`~dyn.tm.accounts.User`'s last name""" - return self._last_name - @last_name.setter - def last_name(self, value): - api_args = {'last_name': value} - self._update(api_args) - - @property - def nickname(self): - """The nickname for the `Contact` associated with this - :class:`~dyn.tm.accounts.User`""" - return self._nickname - @nickname.setter - def nickname(self, value): - api_args = {'nickname': value} - self._update(api_args) - - @property - def organization(self): - """This :class:`~dyn.tm.accounts.User`'s organization""" - return self._organization - @organization.setter - def organization(self, value): - api_args = {'organization': value} - self._update(api_args) - - @property - def phone(self): - """This :class:`~dyn.tm.accounts.User`'s phone number. Can be of the - form: (0) ( country-code ) ( local number ) ( extension ) Only the - country-code (1-3 digits) and local number (at least 7 digits) are - required. The extension can be up to 4 digits. Any non-digits are - ignored. - """ - return self._phone - @phone.setter - def phone(self, value): - api_args = {'phone': value} - self._update(api_args) - - @property - def address(self): - """This :class:`~dyn.tm.accounts.User`'s street address""" - return self._address - @address.setter - def address(self, value): - api_args = {'address': value} - self._update(api_args) - - @property - def address_2(self): - """This :class:`~dyn.tm.accounts.User`'s street address, line 2""" - return self._address_2 - @address_2.setter - def address_2(self, value): - api_args = {'address_2': value} - self._update(api_args) - - @property - def city(self): - """This :class:`~dyn.tm.accounts.User`'s city, part of the user's - address - """ - return self._city - @city.setter - def city(self, value): - api_args = {'city': value} - self._update(api_args) - - @property - def country(self): - """This :class:`~dyn.tm.accounts.User`'s country, part of the user's - address - """ - return self._country - @country.setter - def country(self, value): - api_args = {'country': value} - self._update(api_args) - - @property - def fax(self): - """This :class:`~dyn.tm.accounts.User`'s fax number""" - return self._fax - @fax.setter - def fax(self, value): - api_args = {'fax': value} - self._update(api_args) - - @property - def notify_email(self): - """Email address where this :class:`~dyn.tm.accounts.User` should - receive notifications - """ - return self._notify_email - @notify_email.setter - def notify_email(self, value): - api_args = {'notify_email': value} - self._update(api_args) - - @property - def pager_email(self): - """Email address where this :class:`~dyn.tm.accounts.User` should - receive messages destined for a pager - """ - return self._pager_email - @pager_email.setter - def pager_email(self, value): - api_args = {'pager_email': value} - self._update(api_args) - - @property - def post_code(self): - """This :class:`~dyn.tm.accounts.User`'s postal code, part of the - user's address - """ - return self._post_code - @post_code.setter - def post_code(self, value): - api_args = {'post_code': value} - self._update(api_args) - - @property - def group_name(self): - """A list of permission groups this :class:`~dyn.tm.accounts.User` - belongs to - """ - return self._group_name - @group_name.setter - def group_name(self, value): - api_args = {'group_name': value} - self._update(api_args) - - @property - def permission(self): - """A list of permissions assigned to this - :class:`~dyn.tm.accounts.User` - """ - return self._permission - @permission.setter - def permission(self, value): - api_args = {'permission': value} - self._update(api_args) - - @property - def zone(self): - """A list of zones where this :class:`~dyn.tm.accounts.User`'s - permissions apply - """ - return self._zone - @zone.setter - def zone(self, value): - api_args = {'zone': value} - self._update(api_args) - - @property - def forbid(self): - """A list of forbidden permissions for this - :class:`~dyn.tm.accounts.User` - """ - return self._forbid - @forbid.setter - def forbid(self, value): - """Apply a new list of forbidden permissions for the - :class:`~dyn.tm.accounts.User` - """ - api_args = {'forbid': value} - self._update(api_args) - - @property - def website(self): - """This :class:`~dyn.tm.accounts.User`'s website""" - return self._website - @website.setter - def website(self, value): - api_args = {'website': value} - self._update(api_args) + self._build(response['data']) def block(self): """Blocks this :class:`~dyn.tm.accounts.User` from logging in""" - api_args = {'block': 'True'} - uri = '/User/{}/'.format(self._user_name) - response = DynectSession.get_session().execute(uri, 'PUT', api_args) - self._status = response['data']['status'] + self._update(block=True) def unblock(self): """Restores this :class:`~dyn.tm.accounts.User` to an active status and re-enables their log-in """ - api_args = {'unblock': 'True'} - uri = '/User/{}/'.format(self._user_name) - response = DynectSession.get_session().execute(uri, 'PUT', api_args) - self._status = response['data']['status'] + self._update(unblock=True) def add_permission(self, permission): """Add individual permissions to this :class:`~dyn.tm.accounts.User` @@ -680,7 +352,8 @@ def add_permission(self, permission): :param permission: the permission to add """ self.permissions.append(permission) - uri = '/UserPermissionEntry/{}/{}/'.format(self._user_name, permission) + uri = '/UserPermissionEntry/{0}/{1}/'.format(self._user_name, + permission) DynectSession.get_session().execute(uri, 'POST') def replace_permissions(self, permissions=None): @@ -792,11 +465,6 @@ def delete_forbid_rule(self, permission, zone=None): uri = '/UserForbidEntry/{}/{}/'.format(self._user_name, permission) DynectSession.get_session().execute(uri, 'DELETE', api_args) - def delete(self): - """Delete this :class:`~dyn.tm.accounts.User` from the system""" - uri = '/User/{}/'.format(self._user_name) - DynectSession.get_session().execute(uri, 'DELETE') - def __str__(self): """Custom str method""" return force_unicode(': {}').format(self.user_name) @@ -809,6 +477,10 @@ def __bytes__(self): class PermissionsGroup(object): """A DynECT System Permissions Group object""" + uri = '/UpdateUser/{user_name}/' + session_type = DynectSession + group_name = ImmutableField('group_name') + def __init__(self, group_name, *args, **kwargs): """Create a new permissions Group @@ -824,6 +496,7 @@ def __init__(self, group_name, *args, **kwargs): :param zone: A list of zones where the group's permissions apply """ super(PermissionsGroup, self).__init__() + self.uri = '/PermissionGroup/{}/'.format(group_name) self._group_name = group_name self._description = self._group_type = self._all_users = None self._permission = self._user_name = self._subgroup = self._zone = None @@ -1091,28 +764,21 @@ def __bytes__(self): return bytes(self.__str__()) -class UserZone(object): +class UserZone(APIObject): """A DynECT system UserZoneEntry""" + user_name = ImmutableField('user_name') + zone_name = ImmutableField('zone_name') + def __init__(self, user_name, zone_name, recurse='Y'): - super(UserZone, self).__init__() self._user_name = user_name self._zone_name = zone_name self._recurse = recurse api_args = {'recurse': self._recurse} - uri = '/UserZoneEntry/{}/{}/'.format(self._user_name, self._zone_name) - respnose = DynectSession.get_session().execute(uri, 'POST', api_args) - for key, val in respnose['data'].items(): - setattr(self, '_' + key, val) - - @property - def user_name(self): - """User_name property of :class:`~dyn.tm.accounts.UserZone` object is - read only - """ - return self._user_name - @user_name.setter - def user_name(self, value): - pass + self.uri = '/UserZoneEntry/{}/{}/'.format(self._user_name, + self._zone_name) + respnose = DynectSession.get_session().execute(self.uri, 'POST', + api_args) + self._build(respnose['data']) @property def recurse(self): @@ -1140,18 +806,16 @@ def update_zones(self, zone=None): api_args = {'zone': []} for zone_data in zone: api_args['zone'].append({'zone_name': zone_data}) - uri = '/UserZoneEntry/{}/'.format(self._user_name) - respnose = DynectSession.get_session().execute(uri, 'PUT', api_args) - for key, val in respnose['data'].items(): - setattr(self, '_' + key, val) + respnose = DynectSession.get_session().execute(self.uri, 'PUT', + api_args) + self._build(respnose['data']) def delete(self): """Delete this :class:`~dyn.tm.accounts.UserZone` object from the DynECT System """ api_args = {'recurse': self.recurse} - uri = '/UserZoneEntry/{}/{}/'.format(self._user_name, self._zone_name) - DynectSession.get_session().execute(uri, 'DELETE', api_args) + DynectSession.get_session().execute(self.uri, 'DELETE', api_args) def __str__(self): """Custom str method""" @@ -1163,8 +827,16 @@ def __bytes__(self): return bytes(self.__str__()) -class Notifier(object): +# noinspection PyUnresolvedReferences,PyMissingConstructor +class Notifier(APIObject): """DynECT System Notifier""" + uri = '/Notifier/' + session_type = DynectSession + notifier_id = ImmutableField('notifier_id') + label = StringField('label') + recipients = None + services = None + def __init__(self, *args, **kwargs): """Create a new :class:`~dyn.tm.accounts.Notifier` object @@ -1177,20 +849,16 @@ def __init__(self, *args, **kwargs): :param notifier_id: The system id of this :class:`~dyn.tm.accounts.Notifier` """ - super(Notifier, self).__init__() - self._label = self._recipients = self._services = None - self._notifier_id = self.uri = None if 'api' in kwargs: del kwargs['api'] - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - self.uri = '/Notifier/{}/'.format(self._notifier_id) + self._build(kwargs) elif len(args) + len(kwargs) > 1: self._post(*args, **kwargs) elif len(kwargs) > 0 or 'label' in kwargs: self._post(**kwargs) else: self._get(*args, **kwargs) + self.uri = '/Notifier/{}/'.format(self._notifier_id) def _post(self, label=None, recipients=None, services=None): """Create a new :class:`~dyn.tm.accounts.Notifier` object on the @@ -1204,75 +872,15 @@ def _post(self, label=None, recipients=None, services=None): self._services = services response = DynectSession.get_session().execute(uri, 'POST', self) self._build(response['data']) - self.uri = '/Notifier/{}/'.format(self._notifier_id) def _get(self, notifier_id): """Get an existing :class:`~dyn.tm.accounts.Notifier` object from the DynECT System """ - self._notifier_id = notifier_id self.uri = '/Notifier/{}/'.format(self._notifier_id) response = DynectSession.get_session().execute(self.uri, 'GET') self._build(response['data']) - def _build(self, data): - for key, val in data.items(): - setattr(self, '_' + key, val) - - def _update(self, api_args=None): - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def notifier_id(self): - """The unique System id for this Notifier""" - return self._notifier_id - @notifier_id.setter - def notifier_id(self, value): - pass - - @property - def label(self): - """The label used to identify this :class:`~dyn.tm.accounts.Notifier` - """ - return self._label - @label.setter - def label(self, value): - self._label = value - api_args = {'label': self._label} - self._update(api_args) - - @property - def recipients(self): - """List of Recipients attached to this - :class:`~dyn.tm.accounts.Notifier` - """ - return self._recipients - @recipients.setter - def recipients(self, value): - self._recipients = value - api_args = {'recipients': self._recipients} - self._update(api_args) - - @property - def services(self): - """List of services attached to this - :class:`~dyn.tm.accounts.Notifier` - """ - return self._services - @services.setter - def services(self, value): - self._services = value - api_args = {'services': self._services} - self._update(api_args) - - def delete(self): - """Delete this :class:`~dyn.tm.accounts.Notifier` from the Dynect - System - """ - DynectSession.get_session().execute(self.uri, 'DELETE') - def __str__(self): """Custom str method""" return force_unicode(': {}').format(self.label) @@ -1556,11 +1164,6 @@ def website(self, value): api_args = {'website': self._website} self._update(api_args) - def delete(self): - """Delete this :class:`~dyn.tm.accounts.Contact` from the Dynect System - """ - DynectSession.get_session().execute(self.uri, 'DELETE') - def __str__(self): """Custom str method""" return force_unicode(': {}').format(self.nickname) diff --git a/dyn/tm/zones.py b/dyn/tm/zones.py index abf9f4d..f5ad652 100644 --- a/dyn/tm/zones.py +++ b/dyn/tm/zones.py @@ -56,6 +56,7 @@ def get_all_secondary_zones(): class Zone(object): """A class representing a DynECT Zone""" + def __init__(self, name, *args, **kwargs): """Create a :class:`Zone` object. Note: When creating a new :class:`Zone` if no contact is specified the path to a local zone @@ -179,7 +180,8 @@ def __poll_for_get(self, n_loops=10, xfer=False, xfer_master_ip=None): api_args = {} if xfer_master_ip is not None: api_args['master_ip'] = xfer_master_ip - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get_session().execute(uri, 'GET', + api_args) error_labels = ['running', 'waiting', 'failed', 'canceled'] ok_labels = ['ready', 'unpublished', 'ok'] if response['data']['status'] in error_labels: @@ -211,6 +213,7 @@ def __root_soa(self): def name(self): """The name of this :class:`Zone`""" return self._name + @name.setter def name(self, value): pass @@ -219,6 +222,7 @@ def name(self, value): def fqdn(self): """The name of this :class:`Zone`""" return self._fqdn + @fqdn.setter def fqdn(self, value): pass @@ -231,6 +235,7 @@ def contact(self): if self._contact is None: self._contact = self.__root_soa.rname return self._contact + @contact.setter def contact(self, value): self.__root_soa.rname = value @@ -241,6 +246,7 @@ def ttl(self): if self._ttl is None: self._ttl = self.__root_soa.ttl return self._ttl + @ttl.setter def ttl(self, value): self.__root_soa.ttl = value @@ -249,6 +255,7 @@ def ttl(self, value): def serial(self): """The current serial of this :class:`Zone`""" return self._serial + @serial.setter def serial(self, value): pass @@ -257,6 +264,7 @@ def serial(self, value): def serial_style(self): """The current serial style of this :class:`Zone`""" return self._serial_style + @serial_style.setter def serial_style(self, value): if not value in self.valid_serials: @@ -269,18 +277,19 @@ def status(self): """Convenience property for :class:`Zones`. If a :class:`Zones` is frozen the status will read as `'frozen'`, if the :class:`Zones` is not frozen the status will read as `'active'`. Because the API does not - return information about whether or not a :class:`Zones` is frozen there - will be a few cases where this status will be `None` in order to avoid - guessing what the current status actually is. + return information about whether or not a :class:`Zones` is frozen + there will be a few cases where this status will be `None` in order to + avoid guessing what the current status actually is. """ return self._status + @status.setter def status(self, value): pass def freeze(self): - """Causes the zone to become frozen. Freezing a zone prevents changes to - the zone until it is thawed. + """Causes the zone to become frozen. Freezing a zone prevents changes + to the zone until it is thawed. """ api_args = {'freeze': True} response = DynectSession.get_session().execute(self.uri, 'PUT', @@ -315,7 +324,8 @@ def get_notes(self, offset=None, limit=None): :param offset: The starting point at which to retrieve the notes :param limit: The maximum number of notes to be retrieved - :return: A :class:`list` of :class:`dict` containing :class:`Zone` Notes + :return: A :class:`list` of :class:`dict` containing :class:`Zone` + Notes """ uri = '/ZoneNoteReport/' api_args = {'zone': self.name} @@ -334,8 +344,8 @@ def add_record(self, name=None, record_type='A', *args, **kwargs): :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'. + '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 """ @@ -353,8 +363,8 @@ def add_service(self, name=None, service_type=None, *args, **kwargs): zone :param name: The name of the :class:`Node` where this service will be - attached to or `None` to attach it to the root :class:`Node` of this - :class:`Zone` + attached to or `None` to attach it to the root :class:`Node` of + this :class:`Zone` :param service_type: The type of the service you would like to create. Valid service_type arguments are: 'ActiveFailover', 'DDNS', 'DNSSEC', 'DSF', 'GSLB', 'RDNS', 'RTTM' @@ -437,8 +447,8 @@ def get_all_records_by_type(self, record_type): :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'. + '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', 'CERT': 'CERTRecord', @@ -525,7 +535,8 @@ def get_all_ddns(self): for ddns in response['data']: del ddns['zone'] del ddns['fqdn'] - ddnses.append(DynamicDNS(self._name, self._fqdn, api=False, **ddns)) + ddnses.append( + DynamicDNS(self._name, self._fqdn, api=False, **ddns)) return ddnses def get_all_gslb(self): @@ -557,7 +568,8 @@ def get_all_rdns(self): for rdns in response['data']: del rdns['zone'] del rdns['fqdn'] - rdnses.append(ReverseDNS(self._name, self._fqdn, api=False, **rdns)) + rdnses.append( + ReverseDNS(self._name, self._fqdn, api=False, **rdns)) return rdnses def get_all_rttm(self): @@ -583,8 +595,8 @@ def get_qps(self, start_ts, end_ts=None, breakdown=None, hosts=None, :param start_ts: datetime.datetime instance identifying point in time for the QPS report - :param end_ts: datetime.datetime instance indicating the end of the data - range for the report. Defaults to datetime.datetime.now() + :param end_ts: datetime.datetime instance indicating the end of the + data range for the report. Defaults to datetime.datetime.now() :param breakdown: By default, most data is aggregated together. Valid values ('hosts', 'rrecs', 'zones'). :param hosts: List of hosts to include in the report. @@ -627,6 +639,7 @@ def __ne__(self, other): def __str__(self): """str override""" return force_unicode(': {}').format(self._name) + __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -636,6 +649,7 @@ def __bytes__(self): class SecondaryZone(object): """A class representing DynECT Secondary zones""" + def __init__(self, zone, *args, **kwargs): """Create a :class:`SecondaryZone` object @@ -687,16 +701,18 @@ def _post(self, masters, contact_nickname=None, tsig_key_name=None): def zone(self): """The name of this :class:`SecondaryZone`""" return self._zone + @zone.setter def zone(self, value): pass @property def masters(self): - """A list of IPv4 or IPv6 addresses of the master nameserver(s) for this - zone. - """ + """A list of IPv4 or IPv6 addresses of the master nameserver(s) for + this zone. + """ return self._masters + @masters.setter def masters(self, value): self._masters = value @@ -708,10 +724,11 @@ def masters(self, value): @property def contact_nickname(self): - """Name of the :class:`Contact` that will receive notifications for this - zone - """ + """Name of the :class:`Contact` that will receive notifications for + this zone + """ return self._contact_nickname + @contact_nickname.setter def contact_nickname(self, value): self._contact_nickname = value @@ -727,6 +744,7 @@ def tsig_key_name(self): this zone's master """ return self._tsig_key_name + @tsig_key_name.setter def tsig_key_name(self, value): self._tsig_key_name = value @@ -771,6 +789,7 @@ def delete(self): def __str__(self): """str override""" return force_unicode(': {}').format(self._zone) + __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -785,6 +804,7 @@ class Node(object): :class:`Node` on the DynECT System is by attaching either a record or a service to it. """ + def __init__(self, zone, fqdn=None): """Create a :class:`Node` object @@ -803,8 +823,8 @@ def add_record(self, record_type='A', *args, **kwargs): :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'. + '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 """ @@ -864,7 +884,8 @@ def get_all_records(self): for r_key, r_val in record['rdata'].items(): record[r_key] = r_val record['create'] = False - list_records.append(constructor(self.zone, self.fqdn, **record)) + list_records.append( + constructor(self.zone, self.fqdn, **record)) records[key] = list_records return records @@ -875,8 +896,8 @@ def get_all_records_by_type(self, record_type): :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'. + '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', @@ -927,7 +948,8 @@ def get_any_records(self): for r_key, r_val in record['rdata'].items(): record[r_key] = r_val record['create'] = False - list_records.append(constructor(self.zone, self.fqdn, **record)) + list_records.append( + constructor(self.zone, self.fqdn, **record)) records[key] = list_records return records @@ -941,6 +963,7 @@ def delete(self): def __str__(self): """str override""" return force_unicode(': {}').format(self.fqdn) + __repr__ = __unicode__ = __str__ def __bytes__(self): From 6e58dcf467d915eaefe6e98f30d3f15640e9e0f0 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 12 Nov 2014 19:30:54 -0500 Subject: [PATCH 02/59] Renaming to make it more obvious what's happening --- dyn/core.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/dyn/core.py b/dyn/core.py index 1b75691..1e89a9b 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -71,45 +71,43 @@ def __init__(self, name='', doc=None): self.__doc__ = doc def __get__(self, instance, cls): - print('GET', instance, cls) return getattr(instance, self.private_name, None) def __set__(self, instance, value): - print('SET', instance, value) setattr(instance, self.private_name, value) if hasattr(instance, '_update'): args = {self.name: value} - getattr(instance, '_update')(args) + getattr(instance, '_update')(**args) -class Typed(APIDescriptor): +class TypedAttribute(APIDescriptor): """Type enforced descriptor""" ty = object def __set__(self, instance, value): if not isinstance(value, self.ty): raise TypeError('Expected %s' % self.ty) - super(Typed, self).__set__(instance, value) + super(TypedAttribute, self).__set__(instance, value) -class IntegerField(Typed): +class IntegerAttribute(TypedAttribute): """API Field that may only be an integer type""" ty = int -class StringField(Typed): +class StringAttribute(TypedAttribute): """API Field that may only be a string type""" ty = string_types -class ImmutableField(APIDescriptor): +class ImmutableAttribute(APIDescriptor): """An API field that can not be overridden""" def __set__(self, instance, cls): pass -class ValidatedField(APIDescriptor): +class ValidatedAttribute(APIDescriptor): """An API field whose value can be forced to a specific subset of values""" def __init__(self, name='', doc=None, validator=None): """An API field that must be one of a specific set of values @@ -117,23 +115,23 @@ def __init__(self, name='', doc=None, validator=None): :param name: The name of this field :param validator: An optional list of valid values for this field """ - super(ValidatedField, self).__init__(name, doc) + super(ValidatedAttribute, self).__init__(name, doc) self.validator = validator def __set__(self, instance, value): if self.validator is not None: if value in self.validator: - super(ValidatedField, self).__set__(instance, value) + super(ValidatedAttribute, self).__set__(instance, value) else: # If we have no validator, then assume it's safe to overwrite - super(ValidatedField, self).__set__(instance, value) + super(ValidatedAttribute, self).__set__(instance, value) # class Junk(object): # data = APIDescriptor('data') -# odds = ValidatedField('odds', validator=[1, 3, 5, 7]) -# read_only = ImmutableField('read_only') -# my_int = IntegerField('my_int') -# my_str = StringField('my_str') +# odds = ValidatedAttribute('odds', validator=[1, 3, 5, 7]) +# read_only = ImmutableAttribute('read_only') +# my_int = IntegerAttribute('my_int') +# my_str = StringAttribute('my_str') # # def __init__(self): # self._data = {'serial': 121312, 'ts': 12321111233} From 5d78f01a2a8b6165aa169c88e75a83691518e494 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 12 Nov 2014 19:33:52 -0500 Subject: [PATCH 03/59] Initial pass at porting dyn.tm.accounts to use the new APIObject and descriptor patterns, still need to complete docs and testing --- dyn/tm/accounts.py | 518 ++++++++++----------------------------------- 1 file changed, 111 insertions(+), 407 deletions(-) diff --git a/dyn/tm/accounts.py b/dyn/tm/accounts.py index 1260b0e..ec4afee 100644 --- a/dyn/tm/accounts.py +++ b/dyn/tm/accounts.py @@ -4,7 +4,7 @@ """ from .errors import DynectInvalidArgumentError from .session import DynectSession -from ..core import APIObject, ImmutableField, StringField, APIDescriptor +from ..core import APIObject, ImmutableAttribute, StringAttribute from ..compat import force_unicode __author__ = 'jnappi' @@ -166,10 +166,10 @@ class UpdateUser(APIObject): """ uri = '/UpdateUser/' session_type = DynectSession - user_name = ImmutableField('user_name') - password = StringField('password') - nickname = StringField('nickname') - status = ImmutableField('status') + user_name = ImmutableAttribute('user_name') + password = StringAttribute('password') + nickname = StringAttribute('nickname') + status = ImmutableAttribute('status') def _post(self, nickname, password): """Create a new :class:`~dyn.tm.accounts.UpdateUser` on the DynECT @@ -233,27 +233,27 @@ class User(APIObject): """DynECT System User object""" uri = '/UpdateUser/{user_name}/' session_type = DynectSession - user_name = ImmutableField('user_name') - first_name = StringField('first_name') - last_name = StringField('last_name') - nickname = StringField('nickname') - password = StringField('password') - organization = StringField('organization') - phone = StringField('phone') - address = StringField('address') - address_2 = StringField('address_2') - city = StringField('city') - country = StringField('country') - fax = StringField('fax') - notify_email = StringField('notify_email') - pager_email = StringField('pager_email') - post_code = StringField('post_code') - group_name = StringField('group_name') - permision = StringField('permission') - zone = StringField('zone') - forbid = StringField('forbid') - website = StringField('website') - status = ImmutableField('status') + user_name = ImmutableAttribute('user_name') + first_name = StringAttribute('first_name') + last_name = StringAttribute('last_name') + nickname = StringAttribute('nickname') + password = StringAttribute('password') + organization = StringAttribute('organization') + phone = StringAttribute('phone') + address = StringAttribute('address') + address_2 = StringAttribute('address_2') + city = StringAttribute('city') + country = StringAttribute('country') + fax = StringAttribute('fax') + notify_email = StringAttribute('notify_email') + pager_email = StringAttribute('pager_email') + post_code = StringAttribute('post_code') + group_name = StringAttribute('group_name') + permision = StringAttribute('permission') + zone = StringAttribute('zone') + forbid = StringAttribute('forbid') + website = StringAttribute('website') + status = ImmutableAttribute('status') def __init__(self, user_name, *args, **kwargs): """Create a new :class:`~dyn.tm.accounts.User` object @@ -475,11 +475,18 @@ def __bytes__(self): return bytes(self.__str__()) -class PermissionsGroup(object): +class PermissionsGroup(APIObject): """A DynECT System Permissions Group object""" - uri = '/UpdateUser/{user_name}/' + uri = '/PermissionGroup/{group_name}/' session_type = DynectSession - group_name = ImmutableField('group_name') + group_name = StringAttribute('group_name') + description = StringAttribute('description') + group_type = StringAttribute('group_type') + all_users = StringAttribute('all_users') + permission = StringAttribute('permission') + user_name = StringAttribute('user_name') + subgroup = StringAttribute('subgroup') + zone = StringAttribute('zone') def __init__(self, group_name, *args, **kwargs): """Create a new permissions Group @@ -495,20 +502,9 @@ def __init__(self, group_name, *args, **kwargs): :param subgroup: A list of groups that belong to the permission group :param zone: A list of zones where the group's permissions apply """ - super(PermissionsGroup, self).__init__() - self.uri = '/PermissionGroup/{}/'.format(group_name) + self.uri = self.uri.format(group_name=group_name) self._group_name = group_name - self._description = self._group_type = self._all_users = None - self._permission = self._user_name = self._subgroup = self._zone = None - self.uri = '/PermissionGroup/{}/'.format(self._group_name) - if 'api' in kwargs: - del kwargs['api'] - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - elif len(args) == 0 and len(kwargs) == 0: - self._get() - else: - self._post(*args, **kwargs) + super(PermissionsGroup, self).__init__(*args, **kwargs) def _post(self, description, group_type=None, all_users=None, permission=None, user_name=None, subgroup=None, zone=None): @@ -531,142 +527,35 @@ def _post(self, description, group_type=None, all_users=None, api_args['type'] = val else: api_args[key[1:]] = val - uri = '/PermissionGroup/{}/'.format(self._group_name) - response = DynectSession.get_session().execute(uri, 'POST', api_args) - for key, val in response['data'].items(): - if key == 'type': - setattr(self, '_group_type', val) - elif key == 'zone': - self._zone = [] - for zone in val: - self._zone.append(zone['zone_name']) - else: - setattr(self, '_' + key, val) + response = DynectSession.get_session().execute(self.uri, 'POST', + api_args) + self._build(response['data']) + + def _build(self, data): + """Build the variables in this object by pulling out the data from the + provided data dict. This overrided method pulls out special keys from + the dict, prior to passing off the remaining data to super()._build + """ + setattr(self, '_group_type', data.pop('type')) + zones = data.pop('zone') + self._zone = [] + for zone in zones: + self._zone.append(zone['zone_name']) + super(PermissionsGroup, self)._build(data) def _get(self): """Get an existing :class:`~dyn.tm.accounts.PermissionsGroup` from the DynECT System """ response = DynectSession.get_session().execute(self.uri, 'GET') - for key, val in response['data'].items(): - if key == 'type': - setattr(self, '_group_type', val) - elif key == 'zone': - self._zone = [] - for zone in val: - self._zone.append(zone['zone_name']) - else: - setattr(self, '_' + key, val) - - def _update(self, api_args=None): - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key == 'type': - setattr(self, '_group_type', val) - elif key == 'zone': - self._zone = [] - for zone in val: - self._zone.append(zone['zone_name']) - else: - setattr(self, '_' + key, val) - - @property - def group_name(self): - """The name of this permission group""" - return self._group_name - @group_name.setter - def group_name(self, value): - new_group_name = value - api_args = {'new_group_name': new_group_name, - 'group_name': self._group_name} - self._update(api_args) - self._group_name = new_group_name - self.uri = '/PermissionGroup/{}/'.format(self._group_name) - - @property - def description(self): - """A description of this permission group""" - return self._description - @description.setter - def description(self, value): - self._description = value - api_args = {'group_name': self._group_name, - 'description': self._description} - self._update(api_args) - - @property - def group_type(self): - """The type of this permission group""" - return self._group_type - @group_type.setter - def group_type(self, value): - self._group_type = value - api_args = {'type': self._group_type, - 'group_name': self._group_name} - self._update(api_args) - - @property - def all_users(self): - """If 'Y', all current users will be added to the group. Cannot be - used if user_name is passed in - """ - return self._all_users - @all_users.setter - def all_users(self, value): - self._all_users = value - api_args = {'all_users': self._all_users, - 'group_name': self._group_name} - self._update(api_args) - - @property - def permission(self): - """A list of permissions that this group contains""" - return self._permission - @permission.setter - def permission(self, value): - self._permission = value - api_args = {'permission': self._permission, - 'group_name': self._group_name} - self._update(api_args) - - @property - def user_name(self): - """A list of users that belong to the permission group""" - return self._user_name - @user_name.setter - def user_name(self, value): - self._user_name = value - api_args = {'user_name': self._user_name, - 'group_name': self._group_name} - self._update(api_args) - - @property - def subgroup(self): - """A list of groups that belong to the permission group""" - return self._subgroup - @subgroup.setter - def subgroup(self, value): - self._subgroup = value - api_args = {'subgroup': self._subgroup, - 'group_name': self._group_name} - self._update(api_args) - - @property - def zone(self): - """A list of users that belong to the permission group""" - return self._zone - @zone.setter - def zone(self, value): - self._zone = value - api_args = {'zone': self._zone, - 'group_name': self._group_name} - self._update(api_args) + self._build(response['data']) - def delete(self): - """Delete this permission group""" - uri = '/PermissionGroup/{}/'.format(self._group_name) - DynectSession.get_session().execute(uri, 'DELETE') + def _update(self, **api_args): + """Update this object on the DynECT System""" + if 'group_name' in api_args: + api_args['new_group_name'] = api_args.pop('group_name') + super(PermissionsGroup, self)._update(group_name=self.group_name, + **api_args) def add_permission(self, permission): """Adds individual permissions to the user @@ -687,7 +576,7 @@ def replace_permissions(self, permission=None): api_args = {} if permission is not None: api_args['permission'] = permission - uri = '/PermissionGroupPermissionEntry/{}/'.format(self._group_name) + uri = '/PermissionGroupPermissionEntry/{0}/'.format(self._group_name) DynectSession.get_session().execute(uri, 'PUT', api_args) if permission: self._permission = permission @@ -699,8 +588,8 @@ def remove_permission(self, permission): :param permission: the permission to remove """ - uri = '/PermissionGroupPermissionEntry/{}/{}/'.format(self._group_name, - permission) + uri = '/PermissionGroupPermissionEntry/{0}/{1}/'.format( + self._group_name, permission) DynectSession.get_session().execute(uri, 'DELETE') self._permission.remove(permission) @@ -713,7 +602,8 @@ def add_zone(self, zone, recurse='Y'): of a Zone to this :class:`~dyn.tm.accounts.PermissionsGroup` """ api_args = {'recurse': recurse} - uri = '/PermissionGroupZoneEntry/{}/{}/'.format(self._group_name, zone) + uri = '/PermissionGroupZoneEntry/{0}/{1}/'.format(self._group_name, + zone) DynectSession.get_session().execute(uri, 'POST', api_args) self._zone.append(zone) @@ -725,8 +615,8 @@ def add_subgroup(self, name): to be added to this :class:`~dyn.tm.accounts.PermissionsGroup`'s subgroups """ - uri = '/PermissionGroupSubgroupEntry/{}/{}/'.format(self._group_name, - name) + uri = '/PermissionGroupSubgroupEntry/{0}/{1}/'.format(self._group_name, + name) DynectSession.get_session().execute(uri, 'POST') self._subgroup.append(name) @@ -737,7 +627,7 @@ def update_subgroup(self, subgroups): :param subgroups: The subgroups with updated information """ api_args = {'subgroup': subgroups} - uri = '/PermissionGroupSubgroupEntry/{}/'.format(self._group_name) + uri = '/PermissionGroupSubgroupEntry/{0}/'.format(self._group_name) DynectSession.get_session().execute(uri, 'PUT', api_args) self._subgroup = subgroups @@ -749,14 +639,14 @@ def delete_subgroup(self, name): to be remoevd from this :class:`~dyn.tm.accounts.PermissionsGroup`'s subgroups """ - uri = '/PermissionGroupSubgroupEntry/{}/{}/'.format(self._group_name, - name) + uri = '/PermissionGroupSubgroupEntry/{0}/{1}/'.format(self._group_name, + name) DynectSession.get_session().execute(uri, 'DELETE') self._subgroup.remove(name) def __str__(self): """Custom str method""" - return force_unicode(': {}').format(self.group_name) + return force_unicode(': {0}').format(self.group_name) __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -764,35 +654,24 @@ def __bytes__(self): return bytes(self.__str__()) +# noinspection PyMissingConstructor class UserZone(APIObject): """A DynECT system UserZoneEntry""" - user_name = ImmutableField('user_name') - zone_name = ImmutableField('zone_name') + user_name = ImmutableAttribute('user_name') + zone_name = ImmutableAttribute('zone_name') + recurse = StringAttribute('recurse') def __init__(self, user_name, zone_name, recurse='Y'): self._user_name = user_name self._zone_name = zone_name self._recurse = recurse api_args = {'recurse': self._recurse} - self.uri = '/UserZoneEntry/{}/{}/'.format(self._user_name, - self._zone_name) + self.uri = '/UserZoneEntry/{0}/{1}/'.format(self._user_name, + self._zone_name) respnose = DynectSession.get_session().execute(self.uri, 'POST', api_args) self._build(respnose['data']) - @property - def recurse(self): - """Indicates whether or not permissions should apply to subnodes of - the `zone_name` as well - """ - return self._recurse - @recurse.setter - def recurse(self, value): - self._recurse = value - api_args = {'recurse': self._recurse, 'zone_name': self._zone_name} - uri = '/UserZoneEntry/{}/'.format(self._user_name) - DynectSession.get_session().execute(uri, 'PUT', api_args) - def update_zones(self, zone=None): """Replacement list zones where the user will now have permissions. Pass an empty list or omit the argument to clear the user's zone @@ -819,7 +698,7 @@ def delete(self): def __str__(self): """Custom str method""" - return force_unicode(': {}').format(self.user_name) + return force_unicode(': {0}').format(self.user_name) __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -832,8 +711,8 @@ class Notifier(APIObject): """DynECT System Notifier""" uri = '/Notifier/' session_type = DynectSession - notifier_id = ImmutableField('notifier_id') - label = StringField('label') + notifier_id = ImmutableAttribute('notifier_id') + label = StringAttribute('label') recipients = None services = None @@ -883,7 +762,7 @@ def _get(self, notifier_id): def __str__(self): """Custom str method""" - return force_unicode(': {}').format(self.label) + return force_unicode(': {0}').format(self.label) __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -891,8 +770,27 @@ def __bytes__(self): return bytes(self.__str__()) -class Contact(object): +class Contact(APIObject): """A DynECT System Contact""" + uri = '/Contact/{nickname}/' + session_type = DynectSession + nickname = StringAttribute('nickname') + email = StringAttribute('email') + first_name = StringAttribute('first_name') + last_name = StringAttribute('last_name') + organization = StringAttribute('organization') + phone = StringAttribute('phone') + address = StringAttribute('address') + address_2 = StringAttribute('address_2') + city = StringAttribute('city') + country = StringAttribute('country') + fax = StringAttribute('fax') + notify_email = StringAttribute('notify_email') + pager_email = StringAttribute('pager_email') + post_code = StringAttribute('post_code') + state = StringAttribute('state') + website = StringAttribute('website') + def __init__(self, nickname, *args, **kwargs): """Create a :class:`~dyn.tm.accounts.Contact` object @@ -926,22 +824,11 @@ def __init__(self, nickname, *args, **kwargs): the :class:`~dyn.tm.accounts.Contact`'s address :param website: The :class:`~dyn.tm.accounts.Contact`'s website """ + self.uri = self.uri.format(nickname=nickname) super(Contact, self).__init__() - self._nickname = nickname - self._email = self._first_name = self._last_name = None - self._organization = self._address = self._address_2 = self._city = None - self._country = self._fax = self._notify_email = None - self._pager_email = self._phone = self._post_code = self._state = None - self._website = None - self.uri = '/Contact/{}/'.format(self._nickname) if 'api' in kwargs: del kwargs['api'] - for key, val in kwargs.items(): - if key != '_nickname': - setattr(self, '_' + key, val) - else: - setattr(self, key, val) - self.uri = '/Contact/{}/'.format(self._nickname) + self._build(kwargs) elif len(args) == 0 and len(kwargs) == 0: self._get() else: @@ -971,202 +858,19 @@ def _post(self, email, first_name, last_name, organization, address=None, response = DynectSession.get_session().execute(self.uri, 'POST', self) self._build(response['data']) - def _get(self): - """Get an existing :class:`~dyn.tm.accounts.Contact` from the DynECT - System - """ - response = DynectSession.get_session().execute(self.uri, 'GET') - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - def _build(self, data): - for key, val in data.items(): - setattr(self, '_' + key, val) - - def _update(self, api_args=None): - """Private update method which handles building this - :class:`~dyn.tm.accounts.Contact` object from the API JSON respnose - """ - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) + if '_nickname' in data: + setattr(self, 'nickname', data.pop('_nickname')) + super(Contact, self)._build(data) - @property - def nickname(self): - """This :class:`~dyn.tm.accounts.Contact`'s DynECT System Nickname""" - return self._nickname - @nickname.setter - def nickname(self, value): - self._nickname = value - api_args = {'new_nickname': self._nickname} - self._update(api_args) - - @property - def email(self): - """This :class:`~dyn.tm.accounts.Contact`'s DynECT System Email address - """ - return self._email - @email.setter - def email(self, value): - self._email = value - api_args = {'email': self._email} - self._update(api_args) - - @property - def first_name(self): - """The first name of this :class:`~dyn.tm.accounts.Contact`""" - return self._first_name - @first_name.setter - def first_name(self, value): - self._first_name = value - api_args = {'first_name': self._first_name} - self._update(api_args) - - @property - def last_name(self): - """The last name of this :class:`~dyn.tm.accounts.Contact`""" - return self._last_name - @last_name.setter - def last_name(self, value): - self._last_name = value - api_args = {'last_name': self._last_name} - self._update(api_args) - - @property - def organization(self): - """The organization this :class:`~dyn.tm.accounts.Contact` belongs to - within the DynECT System - """ - return self._organization - @organization.setter - def organization(self, value): - self._organization = value - api_args = {'organization': self._organization} - self._update(api_args) - - @property - def phone(self): - """The phone number associated with this - :class:`~dyn.tm.accounts.Contact` - """ - return self._phone - @phone.setter - def phone(self, value): - self._phone = value - api_args = {'phone': self._phone} - self._update(api_args) - - @property - def address(self): - """This :class:`~dyn.tm.accounts.Contact`'s street address""" - return self._address - @address.setter - def address(self, value): - self._address = value - api_args = {'address': self._address} - self._update(api_args) - - @property - def address_2(self): - """This :class:`~dyn.tm.accounts.Contact`'s street address, line 2""" - return self._address_2 - @address_2.setter - def address_2(self, value): - self._address_2 = value - api_args = {'address_2': self._address_2} - self._update(api_args) - - @property - def city(self): - """This :class:`~dyn.tm.accounts.Contact`'s city""" - return self._city - @city.setter - def city(self, value): - self._city = value - api_args = {'city': self._city} - self._update(api_args) - - @property - def country(self): - """This :class:`~dyn.tm.accounts.Contact`'s Country""" - return self._country - @country.setter - def country(self, value): - self._country = value - api_args = {'country': self._country} - self._update(api_args) - - @property - def fax(self): - """The fax number associated with this - :class:`~dyn.tm.accounts.Contact` - """ - return self._fax - @fax.setter - def fax(self, value): - self._fax = value - api_args = {'fax': self._fax} - self._update(api_args) - - @property - def notify_email(self): - """Email address where this :class:`~dyn.tm.accounts.Contact` should - receive notifications - """ - return self._notify_email - @notify_email.setter - def notify_email(self, value): - self._notify_email = value - api_args = {'notify_email': self._notify_email} - self._update(api_args) - - @property - def pager_email(self): - """Email address where this :class:`~dyn.tm.accounts.Contact` should - receive messages destined for a pager - """ - return self._pager_email - @pager_email.setter - def pager_email(self, value): - self._pager_email = value - api_args = {'pager_email': self._pager_email} - self._update(api_args) - - @property - def post_code(self): - """This :class:`~dyn.tm.accounts.Contacts`'s postal code, part of the - contacts's address - """ - return self._post_code - @post_code.setter - def post_code(self, value): - self._post_code = value - api_args = {'post_code': self._post_code} - self._update(api_args) - - @property - def state(self): - """This :class:`~dyn.tm.accounts.Contact`'s state""" - return self._state - @state.setter - def state(self, value): - self._state = value - api_args = {'state': self._state} - self._update(api_args) - - @property - def website(self): - """This :class:`~dyn.tm.accounts.Contact`'s website""" - return self._website - @website.setter - def website(self, value): - self._website = value - api_args = {'website': self._website} - self._update(api_args) + def _update(self, **api_args): + if 'nickname' in api_args: + api_args['new_nickname'] = api_args.pop('nickname') + super(Contact, self)._update(api_args) def __str__(self): """Custom str method""" - return force_unicode(': {}').format(self.nickname) + return force_unicode(': {0}').format(self.nickname) __repr__ = __unicode__ = __str__ def __bytes__(self): From 06a05ae34137f304b5f77d3301d331742ec6fcf6 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 12 Nov 2014 21:31:19 -0500 Subject: [PATCH 04/59] Initial pass at porting dyn.tm.zones to use the new APIObject and descriptor patterns, still need to complete docs and testing --- dyn/tm/zones.py | 366 +++++++++++------------------------------------- 1 file changed, 82 insertions(+), 284 deletions(-) diff --git a/dyn/tm/zones.py b/dyn/tm/zones.py index f5ad652..5be11aa 100644 --- a/dyn/tm/zones.py +++ b/dyn/tm/zones.py @@ -1,16 +1,17 @@ # -*- coding: utf-8 -*- """This module contains all Zone related API objects.""" import os -import logging from time import sleep from datetime import datetime from .utils import unix_date -from ..compat import force_unicode from .errors import * from .records import * from .session import DynectSession from .services import * +from ..core import (APIObject, IntegerAttribute, StringAttribute, + ListAttribute, ImmutableAttribute, ValidatedAttribute) +from ..compat import force_unicode __author__ = 'jnappi' __all__ = ['get_all_zones', 'Zone', 'SecondaryZone', 'Node'] @@ -54,8 +55,19 @@ def get_all_secondary_zones(): return zones -class Zone(object): +# noinspection PyUnresolvedReferences +class Zone(APIObject): """A class representing a DynECT Zone""" + uri = '/Zone/{zone_name}/' + session_type = DynectSession + zone = ImmutableAttribute('zone') + name = ImmutableAttribute('zone') + fqdn = ImmutableAttribute('fqdn') + serial_style = ValidatedAttribute('serial_style', + validator=('increment', 'epoch', 'day', + 'minute')) + serial = IntegerAttribute('serial') + status = StringAttribute('status') def __init__(self, name, *args, **kwargs): """Create a :class:`Zone` object. Note: When creating a new @@ -75,28 +87,12 @@ def __init__(self, name, *args, **kwargs): :param timeout: The time, in minutes, to wait for a zone xfer to complete """ - super(Zone, self).__init__() - self.valid_serials = ('increment', 'epoch', 'day', 'minute') - self._name = name - self._fqdn = self._name - if self._fqdn and not self._fqdn.endswith('.'): - self._fqdn += '.' - self._contact = self._ttl = self._serial_style = self._serial = None - self._zone = self._status = None + self.uri = self.uri.format(zone_name=name) self.records = {} self.services = {} - self.uri = '/Zone/{}/'.format(self._name) - if 'api' in kwargs: - del kwargs['api'] - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - self._name = self._zone - self.uri = '/Zone/{}/'.format(self._name) - elif len(args) == 0 and len(kwargs) == 0: - self._get() - else: - self._post(*args, **kwargs) - self._status = 'active' + self._contact = self._ttl = None + super(Zone, self).__init__(*args, **kwargs) + self._fqdn = name if name.endswith('.') else '{0}.'.format(name) def _post(self, contact=None, ttl=60, serial_style='increment', file_name=None, master_ip=None, timeout=None): @@ -108,19 +104,15 @@ def _post(self, contact=None, ttl=60, serial_style='increment', elif master_ip is not None: self._xfer(master_ip, timeout) else: - self._contact = contact - self._ttl = ttl - if serial_style not in self.valid_serials: - raise DynectInvalidArgumentError(serial_style, - self.valid_serials) - self._serial_style = serial_style - api_args = {'zone': self._name, - 'rname': self._contact, - 'ttl': self._ttl, - 'serial_style': self._serial_style} + # Assign serial style here to force pre-api validation + self.serial_style = serial_style + + api_args = {'zone': self.name, 'rname': contact, 'ttl': ttl, + 'serial_style': serial_style} response = DynectSession.get_session().execute(self.uri, 'POST', api_args) self._build(response['data']) + self._status = 'active' def _post_with_file(self, file_name): """Create a :class:`Zone` from a RFC1035 style Master file. A ZoneFile @@ -134,7 +126,7 @@ def _post_with_file(self, file_name): raise DynectInvalidArgumentError('Zone File Size', file_size, 'Under 1MB') else: - uri = '/ZoneFile/{}/'.format(self.name) + uri = '/ZoneFile/{0}/'.format(self.name) f = open(full_path, 'r') content = f.read() f.close() @@ -146,13 +138,13 @@ def _xfer(self, master_ip, timeout=None): """Create a :class:`Zone` by ZoneTransfer by providing an optional master_ip argument. """ - uri = '/ZoneTransfer/{}/'.format(self.name) + uri = '/ZoneTransfer/{0}/'.format(self.name) api_args = {'master_ip': master_ip} DynectSession.get_session().execute(uri, 'POST', api_args) time_out = timeout or 10 count = 0 while count < time_out: - response = DynectSession.get_session().execute(uri, 'GET', {}) + response = DynectSession.get_session().execute(uri, 'GET') if response['status'] == 'running' and response['message'] == '': sleep(60) count += 1 @@ -176,7 +168,7 @@ def __poll_for_get(self, n_loops=10, xfer=False, xfer_master_ip=None): sleep(2) count += 1 if not got and xfer: - uri = '/ZoneTransfer/{}/'.format(self.name) + uri = '/ZoneTransfer/{0}/'.format(self.name) api_args = {} if xfer_master_ip is not None: api_args['master_ip'] = xfer_master_ip @@ -191,42 +183,11 @@ def __poll_for_get(self, n_loops=10, xfer=False, xfer_master_ip=None): else: pass # Should never get here - def _get(self): - """Get an existing :class:`Zone` object from the DynECT System""" - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - self._build(response['data']) - - def _build(self, data): - """Build the variables in this object by pulling out the data from data - """ - for key, val in data.items(): - setattr(self, '_' + key, val) - @property def __root_soa(self): """Return the SOA record associated with this Zone""" return self.get_all_records_by_type('SOA')[0] - @property - def name(self): - """The name of this :class:`Zone`""" - return self._name - - @name.setter - def name(self, value): - pass - - @property - def fqdn(self): - """The name of this :class:`Zone`""" - return self._fqdn - - @fqdn.setter - def fqdn(self, value): - pass - @property def contact(self): """The email address of the primary :class:`Contact` associated with @@ -235,7 +196,6 @@ def contact(self): if self._contact is None: self._contact = self.__root_soa.rname return self._contact - @contact.setter def contact(self, value): self.__root_soa.rname = value @@ -246,78 +206,30 @@ def ttl(self): if self._ttl is None: self._ttl = self.__root_soa.ttl return self._ttl - @ttl.setter def ttl(self, value): self.__root_soa.ttl = value - @property - def serial(self): - """The current serial of this :class:`Zone`""" - return self._serial - - @serial.setter - def serial(self, value): - pass - - @property - def serial_style(self): - """The current serial style of this :class:`Zone`""" - return self._serial_style - - @serial_style.setter - def serial_style(self, value): - if not value in self.valid_serials: - raise DynectInvalidArgumentError('serial_style', value, - self.valid_serials) - self.__root_soa.serial_style = value - - @property - def status(self): - """Convenience property for :class:`Zones`. If a :class:`Zones` is - frozen the status will read as `'frozen'`, if the :class:`Zones` is not - frozen the status will read as `'active'`. Because the API does not - return information about whether or not a :class:`Zones` is frozen - there will be a few cases where this status will be `None` in order to - avoid guessing what the current status actually is. - """ - return self._status - - @status.setter - def status(self, value): - pass - def freeze(self): """Causes the zone to become frozen. Freezing a zone prevents changes to the zone until it is thawed. """ - api_args = {'freeze': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - if response['status'] == 'success': - self._status = 'frozen' + self._update(freeze=True) + self._status = 'frozen' def thaw(self): """Causes the zone to become thawed. Thawing a frozen zone allows changes to again be made to the zone. """ - api_args = {'thaw': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - if response['status'] == 'success': - self._status = 'active' + self._update(thaw=True) + self._status = 'active' def publish(self): """Causes all pending changes to become part of the zone. The serial number increments based on its serial style and the data is pushed out to the nameservers. """ - api_args = {'publish': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) + self._update(publish=True) def get_notes(self, offset=None, limit=None): """Generates a report containing the Zone Notes for this :class:`Zone` @@ -415,9 +327,9 @@ def get_all_records(self): :class:`Zone` """ self.records = {} - uri = '/AllRecord/{}/'.format(self._name) + uri = '/AllRecord/{0}/'.format(self.name) if self.fqdn is not None: - uri += '{}/'.format(self.fqdn) + uri += '{0}/'.format(self.fqdn) api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) # Strip out empty record_type lists @@ -435,7 +347,7 @@ def get_all_records(self): for r_key, r_val in record['rdata'].items(): record[r_key] = r_val record['create'] = False - list_records.append(constructor(self._name, self.fqdn, + list_records.append(constructor(self.name, self.fqdn, **record)) records[key] = list_records return records @@ -461,7 +373,7 @@ def get_all_records_by_type(self, record_type): 'NS': 'NSRecord', 'SOA': 'SOARecord', 'SPF': 'SPFRecord', 'SRV': 'SRVRecord', 'TXT': 'TXTRecord'} constructor = RECS[record_type] - uri = '/{}/{}/{}/'.format(names[record_type], self._name, self.fqdn) + uri = '/{0}/{1}/{2}/'.format(names[record_type], self.name, self.fqdn) api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) records = [] @@ -472,8 +384,8 @@ def get_all_records_by_type(self, record_type): for key, val in record['rdata'].items(): record[key] = val del record['rdata'] - record['create'] = False - records.append(constructor(self._name, self.fqdn, **record)) + records.append(constructor(self.name, self.fqdn, api=False, + **record)) return records def get_any_records(self): @@ -483,7 +395,7 @@ def get_any_records(self): if self.fqdn is None: return api_args = {'detail': 'Y'} - uri = '/ANYRecord/{}/{}/'.format(self._name, self.fqdn) + uri = '/ANYRecord/{0}/{1}/'.format(self.name, self.fqdn) response = DynectSession.get_session().execute(uri, 'GET', api_args) # Strip out empty record_type lists record_lists = {label: rec_list for label, rec_list in @@ -499,9 +411,8 @@ def get_any_records(self): # Unpack rdata for r_key, r_val in record['rdata'].items(): record[r_key] = r_val - record['create'] = False - list_records.append(constructor(self._name, self.fqdn, - **record)) + list_records.append(constructor(self.name, self.fqdn, + api=False, **record)) records[key] = list_records return records @@ -511,14 +422,14 @@ def get_all_active_failovers(self): :return: A :class:`List` of :class:`ActiveFailover` Services """ - uri = '/Failover/{}/'.format(self._name) + uri = '/Failover/{0}/'.format(self.name) api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) afos = [] for failover in response['data']: del failover['zone'] del failover['fqdn'] - afos.append(ActiveFailover(self._name, self._fqdn, api=False, + afos.append(ActiveFailover(self.name, self.fqdn, api=False, **failover)) return afos @@ -528,7 +439,7 @@ def get_all_ddns(self): :return: A :class:`List` of :class:`DDNS` Services """ - uri = '/DDNS/{}/'.format(self._name) + uri = '/DDNS/{0}/'.format(self.name) api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) ddnses = [] @@ -536,7 +447,7 @@ def get_all_ddns(self): del ddns['zone'] del ddns['fqdn'] ddnses.append( - DynamicDNS(self._name, self._fqdn, api=False, **ddns)) + DynamicDNS(self.name, self._fqdn, api=False, **ddns)) return ddnses def get_all_gslb(self): @@ -545,14 +456,14 @@ def get_all_gslb(self): :return: A :class:`List` of :class:`GSLB` Services """ - uri = '/GSLB/{}/'.format(self._name) + uri = '/GSLB/{0}/'.format(self.name) api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) gslbs = [] for gslb_svc in response['data']: del gslb_svc['zone'] del gslb_svc['fqdn'] - gslbs.append(GSLB(self._name, self._fqdn, api=False, **gslb_svc)) + gslbs.append(GSLB(self.name, self._fqdn, api=False, **gslb_svc)) return gslbs def get_all_rdns(self): @@ -561,7 +472,7 @@ def get_all_rdns(self): :return: A :class:`List` of :class:`ReverseDNS` Services """ - uri = '/IPTrack/{}/'.format(self._name) + uri = '/IPTrack/{0}/'.format(self.name) api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) rdnses = [] @@ -569,7 +480,7 @@ def get_all_rdns(self): del rdns['zone'] del rdns['fqdn'] rdnses.append( - ReverseDNS(self._name, self._fqdn, api=False, **rdns)) + ReverseDNS(self.name, self._fqdn, api=False, **rdns)) return rdnses def get_all_rttm(self): @@ -578,14 +489,14 @@ def get_all_rttm(self): :return: A :class:`List` of :class:`RTTM` Services """ - uri = '/RTTM/{}/'.format(self._name) + uri = '/RTTM/{0}/'.format(self.name) api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) rttms = [] for rttm_svc in response['data']: del rttm_svc['zone'] del rttm_svc['fqdn'] - rttms.append(RTTM(self._name, self._fqdn, api=False, **rttm_svc)) + rttms.append(RTTM(self.name, self.fqdn, api=False, **rttm_svc)) return rttms def get_qps(self, start_ts, end_ts=None, breakdown=None, hosts=None, @@ -617,28 +528,9 @@ def get_qps(self, start_ts, end_ts=None, breakdown=None, hosts=None, 'POST', api_args) return response['data'] - def delete(self): - """Delete this :class:`Zone` and perform nessecary cleanups""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) - - def __eq__(self, other): - """Equivalence operations for easily pulling a :class:`Zone` out of a - list of :class:`Zone` objects - """ - if isinstance(other, str): - return other == self._name - elif isinstance(other, Zone): - return other.name == self._name - return False - - def __ne__(self, other): - """Non-Equivalence operator""" - return not self.__eq__(other) - def __str__(self): """str override""" - return force_unicode(': {}').format(self._name) + return force_unicode(': {0}').format(self.name) __repr__ = __unicode__ = __str__ @@ -647,8 +539,15 @@ def __bytes__(self): return bytes(self.__str__()) -class SecondaryZone(object): +class SecondaryZone(APIObject): """A class representing DynECT Secondary zones""" + uri = '/Secondary/{zone_name}/' + session_type = DynectSession + zone = StringAttribute('zone') + name = StringAttribute('zone') + masters = ListAttribute('masters') + contact_nickname = StringAttribute('contact_nickname') + tsig_key_name = StringAttribute('tsig_key_name') def __init__(self, zone, *args, **kwargs): """Create a :class:`SecondaryZone` object @@ -661,26 +560,8 @@ def __init__(self, zone, *args, **kwargs): :param tsig_key_name: Name of the TSIG key that will be used to sign transfer requests to this zone's master """ - super(SecondaryZone, self).__init__() - self._zone = self._name = zone - self.uri = '/Secondary/{}/'.format(self._zone) - self._masters = self._contact_nickname = self._tsig_key_name = None - if 'api' in kwargs: - del kwargs['api'] - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - elif len(args) == 0 and len(kwargs) == 0: - self._get() - else: - self._post(*args, **kwargs) - - def _get(self): - """Get a :class:`SecondaryZone` object from the DynECT System""" - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) + self.uri = self.uri.format(zone_name=zone) + super(SecondaryZone, self).__init__(*args, **kwargs) def _post(self, masters, contact_nickname=None, tsig_key_name=None): """Create a new :class:`SecondaryZone` object on the DynECT System""" @@ -694,101 +575,25 @@ def _post(self, masters, contact_nickname=None, tsig_key_name=None): api_args['tsig_key_name'] = self._tsig_key_name response = DynectSession.get_session().execute(self.uri, 'POST', api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - @property - def zone(self): - """The name of this :class:`SecondaryZone`""" - return self._zone - - @zone.setter - def zone(self, value): - pass - - @property - def masters(self): - """A list of IPv4 or IPv6 addresses of the master nameserver(s) for - this zone. - """ - return self._masters - - @masters.setter - def masters(self, value): - self._masters = value - api_args = {'masters': self._masters} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - @property - def contact_nickname(self): - """Name of the :class:`Contact` that will receive notifications for - this zone - """ - return self._contact_nickname - - @contact_nickname.setter - def contact_nickname(self, value): - self._contact_nickname = value - api_args = {'contact_nickname': self._contact_nickname} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - @property - def tsig_key_name(self): - """Name of the TSIG key that will be used to sign transfer requests to - this zone's master - """ - return self._tsig_key_name - - @tsig_key_name.setter - def tsig_key_name(self, value): - self._tsig_key_name = value - api_args = {'tsig_key_name': self._tsig_key_name} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) + self._build(response['data']) def activate(self): """Activates this secondary zone""" - api_args = {'activate': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) + self._update(activate=True) def deactivate(self): """Deactivates this secondary zone""" - api_args = {'deactivate': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) + self._update(deactivate=True) def retransfer(self): """Retransfers this secondary zone from its original provider into Dyn's Managed DNS """ - api_args = {'retransfer': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - def delete(self): - """Delete this :class:`SecondaryZone`""" - api_args = {} - uri = '/Zone/{}/'.format(self._zone) - DynectSession.get_session().execute(uri, 'DELETE', api_args) + self._update(retransfer=True) def __str__(self): """str override""" - return force_unicode(': {}').format(self._zone) + return force_unicode(': {0}').format(self.zone) __repr__ = __unicode__ = __str__ @@ -800,7 +605,7 @@ def __bytes__(self): class Node(object): """Node object. Represents a valid fqdn node within a zone. It should be noted that simply creating a :class:`Node` object does not actually create - anything on the DynECT System. The only way to actively create a + anything on the DynECT System. The only way to really create a :class:`Node` on the DynECT System is by attaching either a record or a service to it. """ @@ -845,16 +650,11 @@ def add_service(self, service_type=None, *args, **kwargs): :param args: Non-keyword arguments to pass to the Record constructor :param kwargs: Keyword arguments to pass to the Record constructor """ - constructors = {'ActiveFailover': ActiveFailover, - 'DDNS': DynamicDNS, - 'DNSSEC': DNSSEC, - 'DSF': TrafficDirector, - 'GSLB': GSLB, - 'RDNS': ReverseDNS, - 'RTTM': RTTM} + objects = {'ActiveFailover': ActiveFailover, 'DDNS': DynamicDNS, + 'DNSSEC': DNSSEC, 'DSF': TrafficDirector, 'GSLB': GSLB, + 'RDNS': ReverseDNS, 'RTTM': RTTM} # noinspection PyCallingNonCallable - service = constructors[service_type](self.zone, self.fqdn, *args, - **kwargs) + service = objects[service_type](self.zone, self.fqdn, *args, **kwargs) self.services.append(service) return service @@ -864,9 +664,9 @@ def get_all_records(self): point on the zone hierarchy """ self.records = {} - uri = '/AllRecord/{}/'.format(self.zone) + uri = '/AllRecord/{0}/'.format(self.zone) if self.fqdn is not None: - uri += '{}/'.format(self.fqdn) + uri += '{0}/'.format(self.fqdn) api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) # Strip out empty record_type lists @@ -883,9 +683,8 @@ def get_all_records(self): # Unpack rdata for r_key, r_val in record['rdata'].items(): record[r_key] = r_val - record['create'] = False list_records.append( - constructor(self.zone, self.fqdn, **record)) + constructor(self.zone, self.fqdn, api=False, **record)) records[key] = list_records return records @@ -910,8 +709,7 @@ def get_all_records_by_type(self, record_type): 'NS': 'NSRecord', 'SOA': 'SOARecord', 'SPF': 'SPFRecord', 'SRV': 'SRVRecord', 'TXT': 'TXTRecord'} constructor = RECS[record_type] - uri = '/{}/{}/{}/'.format(names[record_type], self.zone, - self.fqdn) + uri = '/{0}/{1}/{2}/'.format(names[record_type], self.zone, self.fqdn) api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) records = [] @@ -931,7 +729,7 @@ def get_any_records(self): if self.fqdn is None: return api_args = {'detail': 'Y'} - uri = '/ANYRecord/{}/{}/'.format(self.zone, self.fqdn) + uri = '/ANYRecord/{0}/{1}/'.format(self.zone, self.fqdn) response = DynectSession.get_session().execute(uri, 'GET', api_args) # Strip out empty record_type lists record_lists = {label: rec_list for label, rec_list in @@ -957,12 +755,12 @@ def delete(self): """Delete this node, any records within this node, and any nodes underneath this node """ - uri = '/Node/{}/{}'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'DELETE', {}) + uri = '/Node/{0}/{1}'.format(self.zone, self.fqdn) + DynectSession.get_session().execute(uri, 'DELETE') def __str__(self): """str override""" - return force_unicode(': {}').format(self.fqdn) + return force_unicode(': {0}').format(self.fqdn) __repr__ = __unicode__ = __str__ From 1582ad93361dd0773a151dee211bee0db05fd4e0 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 12 Nov 2014 22:58:44 -0500 Subject: [PATCH 05/59] Initial pass at porting dyn.tm.services to use the new APIObject and descriptor patterns, still need to complete docs and testing --- dyn/tm/services/active_failover.py | 465 ++++++----------------------- dyn/tm/services/ddns.py | 164 +++------- dyn/tm/services/reversedns.py | 188 +++--------- 3 files changed, 174 insertions(+), 643 deletions(-) diff --git a/dyn/tm/services/active_failover.py b/dyn/tm/services/active_failover.py index 5eddbd9..22d6894 100644 --- a/dyn/tm/services/active_failover.py +++ b/dyn/tm/services/active_failover.py @@ -1,17 +1,29 @@ # -*- coding: utf-8 -*- -import logging - from ..utils import Active -from ..errors import DynectInvalidArgumentError from ..session import DynectSession +from ...core import (APIObject, ImmutableAttribute, StringAttribute, + ClassAttribute, IntegerAttribute, + ValidatedListAttribute, ValidatedAttribute) from ...compat import force_unicode __author__ = 'jnappi' __all__ = ['HealthMonitor', 'ActiveFailover'] -class HealthMonitor(object): +class HealthMonitor(APIObject): """A health monitor for an :class:`ActiveFailover` service""" + protocol = ValidatedAttribute('protocol', + validator=('HTTP', 'HTTPS', 'PING', 'SMTP', + 'TCP')) + interval = ValidatedAttribute('interval', validator=(1, 5, 10, 15)) + retries = IntegerAttribute('retries') + timeout = ValidatedAttribute('timeout', validator=(10, 15, 25, 30)) + port = IntegerAttribute('port') + path = StringAttribute('path') + host = StringAttribute('host') + header = StringAttribute('header') + expected = StringAttribute('expected') + def __init__(self, protocol, interval, retries=None, timeout=None, port=None, path=None, host=None, header=None, expected=None): """Create a :class:`HealthMonitor` object @@ -46,9 +58,17 @@ def __init__(self, protocol, interval, retries=None, timeout=None, self._expected = expected self.zone = None self.fqdn = None - self.valid_protocols = ('HTTP', 'HTTPS', 'PING', 'SMTP', 'TCP') - self.valid_intervals = (1, 5, 10, 15) - self.valid_timeouts = (10, 15, 25, 30) + + def _post(self, *args, **kwargs): + """You can't create a HealthMonitor on it's own, so force _post and + _get to be no-ops + """ + pass + _get = _post + + def _update(self, **api_args): + mon_args = {'monitor': api_args} + super(HealthMonitor, self)._update(**mon_args) def to_json(self): """Convert this :class:`HealthMonitor` object to a JSON blob""" @@ -60,145 +80,23 @@ def to_json(self): json_blob[key[1:]] = val return json_blob - def __eq__(self, other): - """eq override for comparing :class:`HealthMonitor` objects to JSON - response hashes or other :class:`DNSSECKey` instances - - :param other: the value to compare this :class:`HealthMonitor` to. Valid - input types: `dict`, :class:`HealthMonitor` - """ - if isinstance(other, dict): - return False - elif isinstance(other, HealthMonitor): - return False - else: - return False + @property + def uri(self): + if self.zone is not None and self.fqdn is not None: + return '/Failover/{0}/{1}/'.format(self.zone, self.fqdn) + raise ValueError @property def status(self): - """Get the current status of this :class:`HealthMonitor` from the DynECT - System + """Get the current status of this :class:`HealthMonitor` from the + DynECT System """ - api_args = {} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - respnose = DynectSession.get_session().execute(uri, 'GET', api_args) + respnose = DynectSession.get_session().execute(self.uri, 'GET') return respnose['data']['status'] - @property - def protocol(self): - """The protocol to monitor""" - return self._protocol - @protocol.setter - def protocol(self, value): - if value not in self.valid_protocols: - raise Exception - self._protocol = value - api_args = {'monitor': {'protocol': self._protocol}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def interval(self): - """How often to run this monitor""" - return self._interval - @interval.setter - def interval(self, value): - if value not in self.valid_intervals: - raise Exception - self._interval = value - api_args = {'monitor': {'interval': self._interval}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def retries(self): - """The number of retries the monitor attempts on failure before giving - up - """ - return self._retries - @retries.setter - def retries(self, value): - self._retries = value - api_args = {'monitor': {'retries': self._retries}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def timeout(self): - """The amount of time in seconds before the connection attempt times - out - """ - return self._timeout - @timeout.setter - def timeout(self, value): - self._timeout = value - api_args = {'monitor': {'timeout': self._timeout}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def port(self): - """For HTTP(S)/SMTP/TCP probes, an alternate connection port""" - return self._port - @port.setter - def port(self, value): - self._port = value - api_args = {'monitor': {'port': self._port}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def path(self): - """For HTTP(S) probes, a specific path to request""" - return self._path - @path.setter - def path(self, value): - self._path = value - api_args = {'monitor': {'path': self._path}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def host(self): - """For HTTP(S) probes, a value to pass in to the Host""" - return self._host - @host.setter - def host(self, value): - self._host = value - api_args = {'monitor': {'host': self._host}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def header(self): - """For HTTP(S) probes, additional header fields/values to pass in, - separated by a newline character - """ - return self._header - @header.setter - def header(self, value): - self._header = value - api_args = {'monitor': {'header': self._header}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def expected(self): - """For HTTP(S) probes, a string to search for in the response. For - SMTP probes, a string to compare the banner against. Failure to find - this string means the monitor will report a down status - """ - return self._expected - @expected.setter - def expected(self, value): - self._expected = value - api_args = {'monitor': {'expected': self._expected}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - def __str__(self): """str override""" - return force_unicode(': {}').format(self._protocol) + return force_unicode(': {0}').format(self._protocol) __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -206,17 +104,37 @@ def __bytes__(self): return bytes(self.__str__()) -class ActiveFailover(object): +# noinspection PyUnresolvedReferences +class ActiveFailover(APIObject): """With Active Failover, we monitor your Primary IP. If a failover event is detected, our system auto switches (hot swaps) to your dedicated back-up IP """ + uri = '/Failover/{zone}/{fqdn}/' + session_type = DynectSession + + zone = ImmutableAttribute('zone') + fqdn = ImmutableAttribute('fqdn') + address = StringAttribute('address') + failover_mode = StringAttribute('failover_mode') + failover_data = StringAttribute('failover_data') + monitor = ClassAttribute('monitor', HealthMonitor) + contact_nickname = StringAttribute('contact_nickname') + auto_recover = StringAttribute('auto_recover') + notify_events = ValidatedListAttribute('notify_events', + validator=('ip', 'svc', 'nosrv')) + syslog_server = StringAttribute('syslog_server') + syslog_port = IntegerAttribute('syslog_port') + syslog_ident = StringAttribute('syslog_ident') + syslog_facility = StringAttribute('syslog_facility') + ttl = IntegerAttribute('ttl') + def __init__(self, zone, fqdn, *args, **kwargs): """Create a new :class:`ActiveFailover` object :param zone: The zone to attach this :class:`ActiveFailover` service to - :param fqdn: The FQDN where this :class:`ActiveFailover` service will be - attached + :param fqdn: The FQDN where this :class:`ActiveFailover` service will + be attached :param address: IPv4 Address or FQDN being monitored by this :class:`ActiveFailover` service :param failover_mode: Indicates the target failover resource type. @@ -239,31 +157,8 @@ def __init__(self, zone, fqdn, *args, **kwargs): :param ttl: Time To Live in seconds of records in the service. Must be less than 1/2 of the Health Probe's monitoring interval """ - super(ActiveFailover, self).__init__() - self.valid_notify_events = ('ip', 'svc', 'nosrv') - self._zone = zone - self._fqdn = fqdn - self._address = self._failover_mode = self._failover_data = None - self._monitor = self._active = None - self._contact_nickname = self._auto_recover = self._notify_events = None - self._syslog_server = self._syslog_port = self._syslog_ident = None - self._syslog_facility = self._ttl = None - self.uri = '/Failover/{}/{}/'.format(self._zone, self._fqdn) - self.api_args = {} - if 'api' in kwargs: - del kwargs['api'] - self._build(kwargs) - elif len(args) == 0 and len(kwargs) == 0: - self._get() - else: - self._post(*args, **kwargs) - - def _get(self): - """Build an object around an existing DynECT Active Failover Service""" - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - self._build(response['data']) + self.uri = self.uri.format(zone=zone, fqdn=fqdn) + super(ActiveFailover, self).__init__(*args, **kwargs) def _post(self, address, failover_mode, failover_data, monitor, contact_nickname, auto_recover=None, notify_events=None, @@ -274,8 +169,8 @@ def _post(self, address, failover_mode, failover_data, monitor, self._failover_mode = failover_mode self._failover_data = failover_data self._monitor = monitor - self._monitor.zone = self._zone - self._monitor.fqdn = self._fqdn + self._monitor.zone = self.zone + self._monitor.fqdn = self.fqdn self._contact_nickname = contact_nickname self._auto_recover = auto_recover self._notify_events = notify_events @@ -284,65 +179,46 @@ def _post(self, address, failover_mode, failover_data, monitor, self._syslog_ident = syslog_ident self._syslog_facility = syslog_facility self._ttl = ttl - api_args = {'address': self._address, - 'failover_mode': self._failover_mode, - 'failover_data': self._failover_data} - for key, val in self.__dict__.items(): - if val is not None and not hasattr(val, '__call__') and \ - key.startswith('_'): - if key != '_user_name': - api_args[key] = val - self.api_args = {'address': self._address, - 'failover_mode': self._failover_mode, - 'failover_data': self._failover_data, - 'monitor': self.monitor.to_json(), - 'contact_nickname': self._contact_nickname} + response = DynectSession.get_session().execute(self.uri, 'POST', self.api_args) self._build(response['data']) + def _update(self, **api_args): + if 'monitor' in api_args: + api_args['monitor'] = api_args['monitor'].to_json() + if 'notify_events' in api_args: + api_args['notify_events'] = ', '.join(api_args['notify_events']) + for key, val in self.api_args: + if key not in api_args: + api_args[key] = val + super(ActiveFailover, self)._update(**api_args) + def _build(self, data): """Build this object from the data returned in an API response""" - for key, val in data.items(): - if key == 'monitor': - self._monitor = HealthMonitor(**val) - elif key == 'active': - self._active = Active(val) - else: - setattr(self, '_' + key, val) - - def _update(self, api_args): - """Update this :class:`ActiveFailover`, via the API, with the args in - api_args - """ - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def zone(self): - """The zone to attach this :class:`ActiveFailover` service to""" - return self._zone - @zone.setter - def zone(self, value): - pass + if 'monitor' in data: + self._monitor = HealthMonitor(**data.pop('monitor')) + if 'active' in data: + self._active = Active(data.pop('active')) + super(ActiveFailover, self)._build(data) @property - def fqdn(self): - """The FQDN where this :class:`ActiveFailover` service will be attached + def api_args(self): + """AFO's required API fields are pretty excessive per call, so use a + property to dynamically generate them on access """ - return self._fqdn - @fqdn.setter - def fqdn(self, value): - pass + return {'address': self.address, 'failover_mode': self.failover_mode, + 'failover_data': self.failover_data, + 'monitor': self.monitor.to_json(), + 'contact_nickname': self.contact_nickname} @property def active(self): - """Return whether or not this :class:`ActiveFailover` service is active. - When setting directly, rather than using activate/deactivate valid - arguments are 'Y' or True to activate, or 'N' or False to deactivate. - Note: If your service is already active and you try to activate it, - nothing will happen. And vice versa for deactivation. + """Return whether or not this :class:`ActiveFailover` service is + active. When setting directly, rather than using activate/deactivate + valid arguments are 'Y' or True to activate, or 'N' or False to + deactivate. Note: If your service is already active and you try to + activate it, nothing will happen. And vice versa for deactivation. :returns: An :class:`Active` object representing the current state of this :class:`ActiveFailover` Service @@ -358,170 +234,17 @@ def active(self, value): elif value in activate and not self.active: self.activate() - @property - def address(self): - """IPv4 Address or FQDN being monitored by this :class:`ActiveFailover` - service - """ - return self._address - @address.setter - def address(self, value): - self._address = value - api_args = {'address': self._address, - 'failover_mode': self._failover_mode, - 'failover_data': self._failover_data, - 'monitor': self.monitor.to_json(), - 'contact_nickname': self._contact_nickname} - self._update(api_args) - - @property - def failover_mode(self): - """Indicates the target failover resource type.""" - return self._failover_mode - @failover_mode.setter - def failover_mode(self, value): - self._failover_mode = value - self.api_args['failover_mode'] = self._failover_mode - self._update(self.api_args) - - @property - def failover_data(self): - """The IPv4 Address or CNAME data for the failover target""" - return self._failover_data - @failover_data.setter - def failover_data(self, value): - self._failover_data = value - self.api_args['failover_data'] = self._failover_data - self._update(self.api_args) - - @property - def monitor(self): - """The :class:`HealthMonitor` for this :class:`ActiveFailover` service - """ - return self._monitor - @monitor.setter - def monitor(self, value): - self._monitor = value - self.api_args['monitor'] = self._monitor.to_json() - self._update(self.api_args) - - @property - def contact_nickname(self): - """Name of contact to receive notifications from this - :class:`ActiveFailover` service - """ - return self._contact_nickname - @contact_nickname.setter - def contact_nickname(self, value): - self._contact_nickname = value - self.api_args['contact_nickname'] = self._contact_nickname - self._update(self.api_args) - - @property - def auto_recover(self): - """Indicates whether this service should restore its original state when - the source IPs resume online status - """ - return self._auto_recover - @auto_recover.setter - def auto_recover(self, value): - self._auto_recover = value - api_args = self.api_args - api_args['auto_recover'] = self._auto_recover - self._update(api_args) - - @property - def notify_events(self): - """A comma separated list of what events trigger notifications""" - return self._notify_events - @notify_events.setter - def notify_events(self, value): - for val in value: - if val not in self.valid_notify_events: - raise DynectInvalidArgumentError('notify_events', val, - self.valid_notify_events) - value = ', '.join(value) - api_args = self.api_args - api_args['notify_events'] = value - self._update(api_args) - - @property - def syslog_server(self): - """The Hostname or IP address of a server to receive syslog - notifications on monitoring events - """ - return self._syslog_server - @syslog_server.setter - def syslog_server(self, value): - self._syslog_server = value - api_args = self.api_args - api_args['syslog_server'] = self._syslog_server - self._update(api_args) - - @property - def syslog_port(self): - """The port where the remote syslog server listens""" - return self._syslog_port - @syslog_port.setter - def syslog_port(self, value): - self._syslog_port = value - api_args = self.api_args - api_args['syslog_port'] = self._syslog_port - self._update(api_args) - - @property - def syslog_ident(self): - """The ident to use when sending syslog notifications""" - return self._syslog_ident - @syslog_ident.setter - def syslog_ident(self, value): - self._syslog_ident = value - api_args = self.api_args - api_args['syslog_ident'] = self._syslog_ident - self._update(api_args) - - @property - def syslog_facility(self): - """The syslog facility to use when sending syslog notifications""" - return self._syslog_facility - @syslog_facility.setter - def syslog_facility(self, value): - self._syslog_facility = value - api_args = self.api_args - api_args['syslog_facility'] = self._syslog_facility - self._update(api_args) - - @property - def ttl(self): - """Time To Live in seconds of records in the service. Must be less than - 1/2 of the Health Probe's monitoring interval - """ - return self._ttl - @ttl.setter - def ttl(self, value): - self._ttl = value - api_args = self.api_args - api_args['ttl'] = self._ttl - self._update(api_args) - def activate(self): """Activate this :class:`ActiveFailover` service""" - api_args = {'activate': True} - self._update(api_args) + self._update(activate=True) def deactivate(self): """Deactivate this :class:`ActiveFailover` service""" - api_args = {'deactivate': True} - self._update(api_args) - - def delete(self): - """Delete this :class:`ActiveFailover` service from the Dynect System""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + self._update(deactivate=True) def __str__(self): """str override""" - return force_unicode(': {}').format(self._fqdn) + return force_unicode(': {0}').format(self.fqdn) __repr__ = __unicode__ = __str__ def __bytes__(self): diff --git a/dyn/tm/services/ddns.py b/dyn/tm/services/ddns.py index 383c218..9a66f17 100644 --- a/dyn/tm/services/ddns.py +++ b/dyn/tm/services/ddns.py @@ -1,22 +1,29 @@ # -*- coding: utf-8 -*- """This module contains API Wrapper implementations of the Dynamic DNS service """ -import logging - from ..utils import Active from ..session import DynectSession from ..accounts import User +from ...core import APIObject, ImmutableAttribute, StringAttribute from ...compat import force_unicode __author__ = 'jnappi' __all__ = ['DynamicDNS'] -class DynamicDNS(object): +class DynamicDNS(APIObject): """DynamicDNS is a service which aliases a dynamic IP Address to a static hostname """ - def __init__(self, zone, fqdn, *args, **kwargs): + uri = '/DDNS/{zone}/{fqdn}/{rr_type}/' + session_type = DynectSession + zone = ImmutableAttribute('zone') + fqdn = ImmutableAttribute('fqdn') + record_type = ImmutableAttribute('record_type') + address = StringAttribute('address') + user = ImmutableAttribute('user') + + def __init__(self, zone, fqdn, record_type, *args, **kwargs): """Create a new :class:`DynamicDNS` service object :param zone: The zone to attach this DDNS Service to @@ -27,86 +34,32 @@ def __init__(self, zone, fqdn, *args, **kwargs): :param user: Name of the user to create, or the name of an existing update user to allow access to this service """ - super(DynamicDNS, self).__init__() - self._zone = zone - self._fqdn = fqdn - self._record_type = self._address = self.uri = self._user = None self._active = None - if 'api' in kwargs: - del kwargs['api'] - for key, val in kwargs.items(): - if key == 'active': - self._active = Active(val) - else: - setattr(self, '_' + key, val) - elif len(args) + len(kwargs) == 1: - self._get(*args, **kwargs) - elif 'record_type' in kwargs and len(kwargs) == 1: - self._get(*args, **kwargs) - else: - self._post(*args, **kwargs) - - def _get(self, record_type=None): - """Build an object around an existing DynECT DynamicDNS Service""" - self._record_type = record_type - self.uri = '/DDNS/{}/{}/{}/'.format(self._zone, self._fqdn, - self._record_type) - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - for key, val in response['data'].items(): - if key == 'active': - self._active = Active(val) - else: - setattr(self, '_' + key, val) - - def _post(self, record_type, address, user=None): + self.uri = self.uri.format(zone=zone, fqdn=fqdn, rr_type=record_type) + self.zone = zone + self.fqdn = fqdn + super(DynamicDNS, self).__init__(*args, **kwargs) + + def _build(self, data): + if 'active' in data: + self._active = Active(data.pop('active')) + if 'ddns' in data and 'new_user' in data: + for ddns_key, ddns_val in data.pop('ddns'): + setattr(self, '_' + ddns_key, ddns_val) + user_data = data.pop('new_user') + username = user_data.pop('user_name') + self._user = User(data.pop(username, api=False, **user_data)) + super(DynamicDNS, self)._build(data) + + def _post(self, address, user=None): """Create a new DynamicDNS Service on the DynECT System""" - self._record_type = record_type - self._address = address - if user: - self._user = user - self.uri = '/DDNS/{}/{}/{}/'.format(self._zone, self._fqdn, - self._record_type) - api_args = {'address': self._address} + api_args = {'address': address} if user: - api_args['user'] = self._user + api_args['user'] = user api_args['full_setup'] = True response = DynectSession.get_session().execute(self.uri, 'POST', api_args) - for key, val in response['data'].items(): - if user: - if key == 'ddns': - for ddns_key, ddns_val in val.items(): - setattr(self, '_' + ddns_key, ddns_val) - if key == 'new_user': - user_name = val['user_name'] - del val['user_name'] - self._user = User(user_name, api=False, **val) - elif key == 'active': - self._active = Active(val) - else: - setattr(self, '_' + key, val) - - @property - def zone(self): - """The zone that this DynamicDNS Service is attached to is a read-only - attribute - """ - return self._zone - @zone.setter - def zone(self, value): - pass - - @property - def fqdn(self): - """The fqdn that this DynamicDNS Service is attached to is a read-only - attribute - """ - return self._fqdn - @fqdn.setter - def fqdn(self, value): - pass + self._build(response['data']) @property def active(self): @@ -129,68 +82,21 @@ def active(self, value): elif value in activate and not self.active: self.activate() - @property - def record_type(self): - """The record_type of a DDNS Service is a read-only attribute""" - return self._record_type - @record_type.setter - def record_type(self, value): - pass - - @property - def user(self): - """The :class:`User` attribute of a DDNS Service is a read-only - attribute""" - return self._user - @user.setter - def user(self, value): - pass - - @property - def address(self): - """IPv4 or IPv6 address for this DynamicDNS service""" - return self._address - @address.setter - def address(self, value): - self._address = value - api_args = {'address': self._address} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - def activate(self): """Activate this Dynamic DNS service""" - api_args = {'activate': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) + self._update(activate=True) def deactivate(self): """Deactivate this Dynamic DNS service""" - api_args = {'deactivate': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) + self._update(deactivate=True) def reset(self): """Resets the abuse count on this Dynamic DNS service""" - api_args = {'reset': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - def delete(self): - """Delete this Dynamic DNS service from the DynECT System""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + self._update(reset=True) def __str__(self): """str override""" - return force_unicode(': {}').format(self._fqdn) + return force_unicode(': {0}').format(self.fqdn) __repr__ = __unicode__ = __str__ def __bytes__(self): diff --git a/dyn/tm/services/reversedns.py b/dyn/tm/services/reversedns.py index 7ccaf55..a3a16fb 100644 --- a/dyn/tm/services/reversedns.py +++ b/dyn/tm/services/reversedns.py @@ -1,16 +1,30 @@ # -*- coding: utf-8 -*- -import logging - from ..utils import Active from ..session import DynectSession +from ...core import (APIObject, StringAttribute, ImmutableAttribute, + ValidatedListAttribute, ListAttribute, IntegerAttribute) from ...compat import force_unicode __author__ = 'jnappi' __all__ = ['ReverseDNS'] -class ReverseDNS(object): +class ReverseDNS(APIObject): """A DynECT ReverseDNS service""" + uri = '/IPTrack/{zone}/{fqdn}/' + _get_length = 1 + session_type = DynectSession + + zone = ImmutableAttribute('zone') + fqdn = ImmutableAttribute('fqdn') + hosts = ListAttribute('hosts') + netmask = StringAttribute('netmask') + ttl = IntegerAttribute('ttl') + record_types = ValidatedListAttribute('record_types', + validator=('A', 'AAAA', 'DynA', + 'DynAAAA')) + iptrack_id = StringAttribute('iptrack_id') + def __init__(self, zone, fqdn, *args, **kwargs): """Create an new :class:`ReverseDNS` object instance @@ -26,89 +40,42 @@ def __init__(self, zone, fqdn, *args, **kwargs): track. Note: Both A and AAAA can not be monitored by the same service """ - super(ReverseDNS, self).__init__() - self._zone = zone - self._fqdn = fqdn - self.valid_record_types = ('A', 'AAAA', 'DynA', 'DynAAAA') - self._hosts = self._netmask = self._ttl = self._record_types = None - self._iptrack_id = self.uri = self._active = None - if 'api' in kwargs: - del kwargs['api'] - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - elif len(args) + len(kwargs) == 1: - self._get(*args, **kwargs) - else: - self._post(*args, **kwargs) + self.uri = self.uri.format(zone=zone, fqdn=fqdn) + super(ReverseDNS, self).__init__(*args, **kwargs) def _post(self, hosts, netmask, ttl='default', record_types=None): """Create a new ReverseDNS Service on the DynECT System""" - self._hosts = hosts - self._netmask = netmask - self._ttl = ttl - self._record_types = [] - if record_types is None: - record_types = ['A'] - for record_type in record_types: - if record_type in self.valid_record_types: - self._record_types.append(record_type) - api_args = {'record_types': self._record_types, - 'hosts': self._hosts, - 'netmask': self._netmask} + record_types = ['A'] if record_types is None else record_types + self.record_types = record_types # Validation check + api_args = {'record_types': self.record_types, 'hosts': hosts, + 'netmask': netmask} if ttl is not None: - api_args['ttl'] = self._ttl - uri = '/IPTrack/{}/{}/'.format(self._zone, self._fqdn) - response = DynectSession.get_session().execute(uri, 'POST', api_args) + api_args['ttl'] = ttl + response = DynectSession.get_session().execute(self.uri, 'POST', + api_args) self._build(response['data']) - self.uri = '/IPTrack/{}/{}/{}/'.format(self._zone, self._fqdn, - self._iptrack_id) def _get(self, service_id): """Build an object around an existing DynECT ReverseDNS Service""" - self._iptrack_id = service_id - uri = '/IPTrack/{}/{}/{}/'.format(self._zone, self._fqdn, - self._iptrack_id) - api_args = {} - response = DynectSession.get_session().execute(uri, 'GET', api_args) - self._build(response['data']) - self.uri = '/IPTrack/{}/{}/{}/'.format(self._zone, self._fqdn, - self._iptrack_id) - - def _update(self, api_args): - """Update this object by making a PUT API call with the provided - api_args - """ - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) + uri = '/IPTrack/{}/{}/{}/'.format(self.zone, self.fqdn, service_id) + response = DynectSession.get_session().execute(uri, 'GET') self._build(response['data']) def _build(self, data): """Build this object based on the data contained in an API response""" - for key, val in data.items(): - if key == 'active': - self._active = Active(val) - else: - setattr(self, '_' + key, val) - - @property - def zone(self): - """The zone that this ReverseDNS Service is attached to is a read-only - attribute - """ - return self._zone - @zone.setter - def zone(self, value): - pass - - @property - def fqdn(self): - """The fqdn that this ReverseDNS Service is attached to is a read-only - attribute - """ - return self._fqdn - @fqdn.setter - def fqdn(self, value): - pass + if 'active' in data: + self._active = Active(data.pop('active')) + super(ReverseDNS, self)._build(data) + self.uri += '{0}/'.format(self.iptrack_id) + + def _update(self, **api_args): + special_cases = 'record_types', 'hosts', 'ttl', 'netmask' + for key in api_args: + if key in special_cases: + api_args['record_types'] = self.record_types + api_args['hosts'] = self.hosts + break # Only need to check/assign them once + super(ReverseDNS, self)._update(**api_args) @property def active(self): @@ -124,88 +91,23 @@ def active(self): return self._active @active.setter def active(self, value): - deactivate = ('N', False) - activate = ('Y', True) + deactivate, activate = ('N', False), ('Y', True) if value in deactivate and self.active: self.deactivate() elif value in activate and not self.active: self.activate() - @property - def iptrack_id(self): - """The unique System id for this service. This is a read-only property. - """ - return self._iptrack_id - @iptrack_id.setter - def iptrack_id(self, value): - pass - - @property - def record_types(self): - """Types of records to track""" - return self._record_types - @record_types.setter - def record_types(self, value): - self._record_types = value - api_args = {'record_types': self._record_types, - 'hosts': self._hosts} - self._update(api_args) - - @property - def hosts(self): - """Hostnames of zones in your account where you want to track records""" - return self._hosts - @hosts.setter - def hosts(self, value): - self._hosts = value - api_args = {'record_types': self._record_types, - 'hosts': self._hosts} - self._update(api_args) - - @property - def ttl(self): - """TTL for the created PTR records. Omit to use zone default""" - return int(self._ttl) - @ttl.setter - def ttl(self, value): - self._ttl = value - api_args = {'record_types': self._record_types, - 'hosts': self._hosts, - 'ttl': self._ttl} - self._update(api_args) - - @property - def netmask(self): - """A netmask to match A/AAAA rdata against. Matched records will get - PTR records, any others won't - """ - return self._netmask - @netmask.setter - def netmask(self, value): - self._netmask = value - api_args = {'record_types': self._record_types, - 'hosts': self._hosts, - 'netmask': self._netmask} - self._update(api_args) - def activate(self): """Activate this ReverseDNS service""" - api_args = {'activate': True} - self._update(api_args) + self._update(activate=True) def deactivate(self): """Deactivate this ReverseDNS service""" - api_args = {'deactivate': True} - self._update(api_args) - - def delete(self): - """Delete this ReverseDNS service from the DynECT System""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + self._update(deactivate=True) def __str__(self): """str override""" - return force_unicode(': {}').format(self._fqdn) + return force_unicode(': {0}').format(self.fqdn) __repr__ = __unicode__ = __str__ def __bytes__(self): From 95ada34c0c72ef97d6db95731b106c99cb3898c4 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 12 Nov 2014 22:59:24 -0500 Subject: [PATCH 06/59] Throwing in a couple logical changes for generically processing APIObjects --- dyn/core.py | 59 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/dyn/core.py b/dyn/core.py index 1e89a9b..5bc4094 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -10,7 +10,6 @@ import logging import threading from datetime import datetime -from collections import OrderedDict from . import __version__ from .compat import (HTTPConnection, HTTPSConnection, HTTPException, json, @@ -64,11 +63,10 @@ class Singleton(_Singleton('SingletonMeta', (object,), {})): class APIDescriptor(object): - def __init__(self, name='', doc=None): + def __init__(self, name=''): super(APIDescriptor, self).__init__() self.name = name self.private_name = '_' + self.name - self.__doc__ = doc def __get__(self, instance, cls): return getattr(instance, self.private_name, None) @@ -80,6 +78,13 @@ def __set__(self, instance, value): getattr(instance, '_update')(**args) +class ImmutableAttribute(APIDescriptor): + """An API Attribute that can not be overridden""" + + def __set__(self, instance, cls): + pass + + class TypedAttribute(APIDescriptor): """Type enforced descriptor""" ty = object @@ -91,47 +96,71 @@ def __set__(self, instance, value): class IntegerAttribute(TypedAttribute): - """API Field that may only be an integer type""" + """API Attribute that may only be an integer type""" ty = int class StringAttribute(TypedAttribute): - """API Field that may only be a string type""" + """API Attribute that may only be a string type""" ty = string_types -class ImmutableAttribute(APIDescriptor): - """An API field that can not be overridden""" +class ListAttribute(TypedAttribute): + """API Attribute that may only be a list""" + ty = list - def __set__(self, instance, cls): - pass + +class ClassAttribute(TypedAttribute): + """API Attribute that can force type checking on an arbitrary type. Useful + for API attributes that are wrapped in something else to make them more + easy to use + """ + def __init__(self, name='', class_type=object): + self.ty = class_type + super(ClassAttribute, self).__init__(name) class ValidatedAttribute(APIDescriptor): - """An API field whose value can be forced to a specific subset of values""" - def __init__(self, name='', doc=None, validator=None): + """An API Attribute whose value can be forced to a specific subset of + values + """ + def __init__(self, name='', validator=None): """An API field that must be one of a specific set of values :param name: The name of this field :param validator: An optional list of valid values for this field """ - super(ValidatedAttribute, self).__init__(name, doc) + super(ValidatedAttribute, self).__init__(name) self.validator = validator def __set__(self, instance, value): + # Get around circular imports by importing here + from dyn.tm.errors import DynectInvalidArgumentError + if self.validator is not None: if value in self.validator: super(ValidatedAttribute, self).__set__(instance, value) + else: + raise DynectInvalidArgumentError(value, self.validator) else: # If we have no validator, then assume it's safe to overwrite super(ValidatedAttribute, self).__set__(instance, value) +class ValidatedListAttribute(ValidatedAttribute, ListAttribute): + def __set__(self, instance, value): + l = getattr(instance, self.private_name, []) + for item in l: + super(ValidatedListAttribute, self).__set__(instance, item) + setattr(instance, self.private_name, value) + + # class Junk(object): # data = APIDescriptor('data') # odds = ValidatedAttribute('odds', validator=[1, 3, 5, 7]) # read_only = ImmutableAttribute('read_only') # my_int = IntegerAttribute('my_int') # my_str = StringAttribute('my_str') +# my_list = ListAttribute('my_list') # # def __init__(self): # self._data = {'serial': 121312, 'ts': 12321111233} @@ -139,6 +168,7 @@ def __set__(self, instance, value): # self._read_only = 'passw0rd' # self._my_int = 0 # self._my_str = 'lulz' +# self._my_list = [0, 1, 2] # noinspection PyUnusedLocal @@ -156,6 +186,7 @@ class APIObject(object): attributes that start with '_' but not '__'. """ uri = '' + _get_length = 0 session_type = None def __init__(self, *args, **kwargs): @@ -163,8 +194,8 @@ def __init__(self, *args, **kwargs): if 'api' in kwargs: del kwargs['api'] self._build(kwargs) - elif len(args) == 0 and len(kwargs) == 0: # Safe default behaviour - self._get() + elif len(args) == 0 and len(kwargs) == self._get_length: + self._get(*args, **kwargs) else: self._post(*args, **kwargs) From 62c8682057d2f51319c73a57391c26b9083b2311 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 12 Nov 2014 23:01:31 -0500 Subject: [PATCH 07/59] PEP8 fixes --- dyn/tm/reports.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/dyn/tm/reports.py b/dyn/tm/reports.py index 3683cdc..11d4b74 100644 --- a/dyn/tm/reports.py +++ b/dyn/tm/reports.py @@ -65,12 +65,10 @@ def get_rttm_log(zone_name, fqdn, start_ts, end_ts=None): :return: A *dict* containing log report data """ end_ts = end_ts or datetime.now() - api_args = {'zone': zone_name, - 'fqdn': fqdn, - 'start_ts': unix_date(start_ts), - 'end_ts': unix_date(end_ts)} - response = DynectSession.get_session().execute('/RTTMLogReport/', - 'POST', api_args) + api_args = {'zone': zone_name, 'fqdn': fqdn, + 'start_ts': unix_date(start_ts), 'end_ts': unix_date(end_ts)} + response = DynectSession.get_session().execute('/RTTMLogReport/', 'POST', + api_args) return response['data'] @@ -84,9 +82,7 @@ def get_rttm_rrset(zone_name, fqdn, ts): report :return: A *dict* containing rrset report data """ - api_args = {'zone': zone_name, - 'fqdn': fqdn, - 'ts': unix_date(ts)} + api_args = {'zone': zone_name, 'fqdn': fqdn, 'ts': unix_date(ts)} response = DynectSession.get_session().execute('/RTTMRRSetReport/', 'POST', api_args) return response['data'] @@ -108,8 +104,7 @@ def get_qps(start_ts, end_ts=None, breakdown=None, hosts=None, rrecs=None, :return: A *str* with CSV data """ end_ts = end_ts or datetime.now() - api_args = {'start_ts': unix_date(start_ts), - 'end_ts': unix_date(end_ts)} + api_args = {'start_ts': unix_date(start_ts), 'end_ts': unix_date(end_ts)} if breakdown is not None: api_args['breakdown'] = breakdown if hosts is not None: @@ -118,8 +113,8 @@ def get_qps(start_ts, end_ts=None, breakdown=None, hosts=None, rrecs=None, api_args['rrecs'] = rrecs if zones is not None: api_args['zones'] = zones - response = DynectSession.get_session().execute('/QPSReport/', - 'POST', api_args) + response = DynectSession.get_session().execute('/QPSReport/', 'POST', + api_args) return response['data'] @@ -137,6 +132,6 @@ def get_zone_notes(zone_name, offset=None, limit=None): api_args['offset'] = offset if limit: api_args['limit'] = limit - response = DynectSession.get_session().execute('/ZoneNoteReport/', - 'POST', api_args) + response = DynectSession.get_session().execute('/ZoneNoteReport/', 'POST', + api_args) return response['data'] From cc815fd5faf34ac02a7ee3170911b9a66ab5c1f1 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 13 Nov 2014 22:06:33 -0500 Subject: [PATCH 08/59] Small AFO fixes --- dyn/tm/services/active_failover.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dyn/tm/services/active_failover.py b/dyn/tm/services/active_failover.py index 22d6894..7d23cda 100644 --- a/dyn/tm/services/active_failover.py +++ b/dyn/tm/services/active_failover.py @@ -12,6 +12,7 @@ class HealthMonitor(APIObject): """A health monitor for an :class:`ActiveFailover` service""" + session_type = DynectSession protocol = ValidatedAttribute('protocol', validator=('HTTP', 'HTTPS', 'PING', 'SMTP', 'TCP')) From 95595bcaf389b698309f0c532eb27223dbce75a4 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 13 Nov 2014 22:06:59 -0500 Subject: [PATCH 09/59] Initial pass at porting dyn.tm.services.dnssec and gslb to use the new APIObject and descriptor patterns, still need to complete docs and testing --- dyn/tm/services/dnssec.py | 131 ++---- dyn/tm/services/gslb.py | 872 ++++++++------------------------------ 2 files changed, 222 insertions(+), 781 deletions(-) diff --git a/dyn/tm/services/dnssec.py b/dyn/tm/services/dnssec.py index fdf5601..eda34c0 100644 --- a/dyn/tm/services/dnssec.py +++ b/dyn/tm/services/dnssec.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -import logging from datetime import datetime from ..utils import APIList, Active, unix_date -from ..errors import DynectInvalidArgumentError from ..session import DynectSession +from ...core import (APIObject, ImmutableAttribute, StringAttribute, + ValidatedListAttribute) from ...compat import force_unicode __author__ = 'jnappi' @@ -82,7 +82,7 @@ def _update(self, data): def __str__(self): """str override""" - return force_unicode(': {}').format(self.algorithm) + return force_unicode(': {0}').format(self.algorithm) __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -90,8 +90,16 @@ def __bytes__(self): return bytes(self.__str__()) -class DNSSEC(object): +class DNSSEC(APIObject): """A DynECT System DNSSEC Service""" + uri = '/DNSSEC/{zone_name}/' + session_type = DynectSession + zone = ImmutableAttribute('zone') + contact_nickname = StringAttribute('contact_nickname') + notify_events = ValidatedListAttribute('notify_events', + validator=('create', 'expire', + 'warning')) + def __init__(self, zone, *args, **kwargs): """Create a :class:`DNSSEC` object @@ -103,20 +111,9 @@ def __init__(self, zone, *args, **kwargs): "expire" (a key was automatically expired), or "warning" (early warnings (2 weeks, 1 week, 1 day) of events) """ - super(DNSSEC, self).__init__() - self.valid_notify_events = ('create', 'expire', 'warning') - self._zone = zone - self._contact_nickname = self._notify_events = None - self._keys = APIList(DynectSession.get_session, 'keys') self._active = None - self.uri = '/DNSSEC/{}/'.format(self._zone) - if 'api' in kwargs: - del kwargs['api'] - self._build(kwargs) - elif len(args) == 0 and len(kwargs) == 0: - self._get() - else: - self._post(*args, **kwargs) + self.uri = self.uri.format(zone_name=zone) + super(DNSSEC, self).__init__(*args, **kwargs) self._keys.uri = self.uri def _post(self, keys, contact_nickname, notify_events=None): @@ -140,42 +137,25 @@ def _post(self, keys, contact_nickname, notify_events=None): api_args) self._build(response['data']) - def _get(self): - """Update this object from an existing :class:`DNSSEC` service from the - Dynect System. - """ - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - self._build(response['data']) - def _build(self, data): """Iterate over API data responses and update this object according to the data returned """ - for key, val in data.items(): - if key == 'keys': - self._keys = APIList(DynectSession.get_session, 'keys') - for key_data in val: - key_data['key_type'] = key_data['type'] - del key_data['type'] - self._keys.append(DNSSECKey(**key_data)) - elif key == 'active': - self._active = Active(val) - else: - setattr(self, '_' + key, val) - self.uri = '/DNSSEC/{}/'.format(self._zone) - self._keys.uri = self.uri - - @property - def zone(self): - """The name of the zone where this service exists. This is a read-only - property - """ - return self._zone - @zone.setter - def zone(self, value): - pass + if 'keys' in data: + keys = data.pop('keys') + self._keys = APIList(DynectSession.get_session, 'keys') + for key in keys: + key['key_type'] = key.pop('type') + self._keys.append(DNSSECKey(**key)) + self._keys.uri = self.uri + if 'active' in data: + self._active = Active(data.pop('active')) + super(DNSSEC, self)._build(data) + + def _update(self, **api_args): + if 'notify_events' in api_args: + api_args['notify_events'] = ', '.join(api_args['notify_events']) + super(DNSSEC, self)._update(**api_args) @property def active(self): @@ -188,7 +168,7 @@ def active(self): :returns: An :class:`Active` object representing the current state of this :class:`DNSSEC` Service """ - self._get() # Do a get to ensure the most up-to-date status is returned + self._get() # Do a get to ensure we have the most up-to-date status return self._active @active.setter def active(self, value): @@ -199,38 +179,6 @@ def active(self, value): elif value in activate and not self.active: self.activate() - @property - def contact_nickname(self): - """Name of contact to receive notifications""" - return self._contact_nickname - @contact_nickname.setter - def contact_nickname(self, value): - self._contact_nickname = value - api_args = {'contact_nickname': self._contact_nickname} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def notify_events(self): - """A list of events that trigger notifications. Valid values are: - create (a new version of a key was created), expire (a key was - automatically expired), warning (early warnings (2 weeks, 1 week, 1 day) - of events) - """ - return self._notify_events - @notify_events.setter - def notify_events(self, value): - for val in value: - if val not in self.valid_notify_events: - raise DynectInvalidArgumentError('notify_events', val, - self.valid_notify_events) - value = ', '.join(value) - api_args = {'notify_events': value} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - @property def keys(self): """A List of :class:`DNSSECKey`'s associated with this :class:`DNSSEC` @@ -250,17 +198,11 @@ def keys(self, value): def activate(self): """Activate this :class:`DNSSEC` service""" - api_args = {'activate': 'Y'} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) + self._update(activate='Y') def deactivate(self): """Deactivate this :class:`DNSSEC` service""" - api_args = {'deactivate': 'Y'} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) + self._update(deactivate='Y') def timeline_report(self, start_ts=None, end_ts=None): """Generates a report of events this :class:`DNSSEC` service has @@ -272,7 +214,7 @@ def timeline_report(self, start_ts=None, end_ts=None): for the end of the timeline report. Defaults to datetime.datetime.now() """ - api_args = {'zone': self._zone} + api_args = {'zone': self.zone} if start_ts is not None: api_args['start_ts'] = unix_date(start_ts) if end_ts is not None: @@ -283,14 +225,9 @@ def timeline_report(self, start_ts=None, end_ts=None): response = DynectSession.get_session().execute(uri, 'POST', api_args) return response['data'] - def delete(self): - """Delete this :class:`DNSSEC` Service from the DynECT System""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) - def __str__(self): """str override""" - return force_unicode(': {}').format(self._zone) + return force_unicode(': {0}').format(self.zone) __repr__ = __unicode__ = __str__ def __bytes__(self): diff --git a/dyn/tm/services/gslb.py b/dyn/tm/services/gslb.py index 2826e1d..e44a9c8 100644 --- a/dyn/tm/services/gslb.py +++ b/dyn/tm/services/gslb.py @@ -1,17 +1,30 @@ # -*- coding: utf-8 -*- -import logging - from ..utils import APIList -from ..errors import DynectInvalidArgumentError from ..session import DynectSession +from ...core import (APIObject, ImmutableAttribute, StringAttribute, + ValidatedAttribute, IntegerAttribute, ClassAttribute, + ListAttribute) from ...compat import force_unicode __author__ = 'jnappi' __all__ = ['Monitor', 'GSLBRegionPoolEntry', 'GSLBRegion', 'GSLB'] -class Monitor(object): +class Monitor(APIObject): """A :class:`Monitor` for a GSLB Service""" + session_type = DynectSession + protocol = ValidatedAttribute('protocol', + validator=('HTTP', 'HTTPS', 'PING', 'SMTP', + 'TCP')) + interval = ValidatedAttribute('interval', validator=(1, 5, 10, 15)) + retries = IntegerAttribute('retries') + timeout = ValidatedAttribute('timeout', validator=(10, 15, 25, 30)) + port = IntegerAttribute('port') + path = StringAttribute('path') + host = StringAttribute('host') + header = StringAttribute('header') + expected = StringAttribute('expected') + def __init__(self, protocol, interval, retries=None, timeout=None, port=None, path=None, host=None, header=None, expected=None): """Create a :class:`Monitor` object @@ -46,159 +59,46 @@ def __init__(self, protocol, interval, retries=None, timeout=None, self._expected = expected self.zone = None self.fqdn = None - self.valid_protocols = ('HTTP', 'HTTPS', 'PING', 'SMTP', 'TCP') - self.valid_intervals = (1, 5, 10, 15) - self.valid_timeouts = (10, 15, 25, 30) + + def _post(self, *args, **kwargs): + """You can't create a HealthMonitor on it's own, so force _post and + _get to be no-ops + """ + pass + _get = _post + + def _update(self, **api_args): + mon_args = {'monitor': api_args} + super(Monitor, self)._update(**mon_args) def to_json(self): """Convert this :class:`HealthMonitor` object to a JSON blob""" - json_blob = {'protocol': self._protocol, - 'interval': self._interval} + json_blob = {'protocol': self.protocol, + 'interval': self.interval} for key, val in self.__dict__.items(): if val is not None and not hasattr(val, '__call__') and \ key.startswith('_'): json_blob[key[1:]] = val return json_blob - def __eq__(self, other): - """eq override for comparing :class:`HealthMonitor` objects to JSON - response hashes or other :class:`HealthMonitor` instances - - :param other: the value to compare this :class:`HealthMonitor` to. Valid - input types: `dict`, :class:`HealthMonitor` - """ - if isinstance(other, dict): - return False - elif isinstance(other, Monitor): - return False - else: - return False + @property + def uri(self): + if self.zone is not None and self.fqdn is not None: + return '/GSLB/{0}/{1}/'.format(self.zone, self.fqdn) + raise ValueError @property def status(self): - """Get the current status of this :class:`HealthMonitor` from the DynECT - System + """Get the current status of this :class:`Monitor` from the + DynECT System """ - api_args = {} uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - respnose = DynectSession.get_session().execute(uri, 'GET', api_args) + respnose = DynectSession.get_session().execute(uri, 'GET') return respnose['data']['status'] - @property - def protocol(self): - """The protocol to monitor""" - return self._protocol - @protocol.setter - def protocol(self, value): - if value not in self.valid_protocols: - raise Exception - self._protocol = value - api_args = {'monitor': {'protocol': self._protocol}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def interval(self): - """How often to run this monitor""" - return self._interval - @interval.setter - def interval(self, value): - if value not in self.valid_intervals: - raise Exception - self._interval = value - api_args = {'monitor': {'interval': self._interval}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def retries(self): - """The number of retries the monitor attempts on failure before giving - up - """ - return self._retries - @retries.setter - def retries(self, value): - self._retries = value - api_args = {'monitor': {'retries': self._retries}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def timeout(self): - """The amount of time in seconds before the connection attempt times - out - """ - return self._timeout - @timeout.setter - def timeout(self, value): - self._timeout = value - api_args = {'monitor': {'timeout': self._timeout}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def port(self): - """For HTTP(S)/SMTP/TCP probes, an alternate connection port""" - return self._port - @port.setter - def port(self, value): - self._port = value - api_args = {'monitor': {'port': self._port}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def path(self): - """For HTTP(S) probes, a specific path to request""" - return self._path - @path.setter - def path(self, value): - self._path = value - api_args = {'monitor': {'path': self._path}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def host(self): - """For HTTP(S) probes, a value to pass in to the Host""" - return self._host - @host.setter - def host(self, value): - self._host = value - api_args = {'monitor': {'host': self._host}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def header(self): - """For HTTP(S) probes, additional header fields/values to pass in, - separated by a newline character - """ - return self._header - @header.setter - def header(self, value): - self._header = value - api_args = {'monitor': {'header': self._header}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - - @property - def expected(self): - """For HTTP(S) probes, a string to search for in the response. For - SMTP probes, a string to compare the banner against. Failure to find - this string means the monitor will report a down status - """ - return self._expected - @expected.setter - def expected(self, value): - self._expected = value - api_args = {'monitor': {'expected': self._expected}} - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'PUT', api_args) - def __str__(self): """str override""" - return force_unicode(': {}').format(self._protocol) + return force_unicode(': {0}').format(self.protocol) __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -206,8 +106,18 @@ def __bytes__(self): return bytes(self.__str__()) -class GSLBRegionPoolEntry(object): +class GSLBRegionPoolEntry(APIObject): """:class:`GSLBRegionPoolEntry`""" + uri = '/GSLBRegionPoolEntry/{zone}/{fqdn}/{region}/{address}/' + session_type = DynectSession + zone = ImmutableAttribute('zone') + fqdn = ImmutableAttribute('fqdn') + region_code = ImmutableAttribute('region_code') + label = StringAttribute('label') + weight = ValidatedAttribute('weight', validator=range(1, 15)) + serve_mode = ValidatedAttribute('serve_mode', validator=('always', 'obey', + 'remove', 'no')) + def __init__(self, zone, fqdn, region_code, address, *args, **kwargs): """Create a :class:`GSLBRegionPoolEntry` object @@ -224,167 +134,52 @@ def __init__(self, zone, fqdn, region_code, address, *args, **kwargs): :param serve_mode: Sets the behavior of this particular record. Must be one of 'always', 'obey', 'remove', 'no' """ - super(GSLBRegionPoolEntry, self).__init__() - self.valid_serve_modes = ('always', 'obey', 'remove', 'no') - self.valid_weight = range(1, 15) - self._zone = zone - self._fqdn = fqdn - self._region_code = region_code - self._address = address - self._label = self._weight = self._serve_mode = None - uri = '/GSLBRegionPoolEntry/{}/{}/{}/{}/' - self.uri = uri.format(self._zone, self._fqdn, self._region_code, - self._address) - if len(args) == 0 and len(kwargs) == 0: - self._get() - else: - for key, val in kwargs.items(): - setattr(self, '_' + key, val) + self.zone, self.fqdn, = zone, fqdn + self.region_code, self.address = region_code, address + self.uri = self.uri.format(zone=zone, fqdn=fqdn, region=region_code, + address=address) + super(GSLBRegionPoolEntry, self).__init__(*args, **kwargs) def _post(self, label=None, weight=None, serve_mode=None): """Create a new :class:`GSLBRegionPoolEntry` on the DynECT System""" - self._label = label - self._weight = weight - self._serve_mode = serve_mode - uri = '/GSLBRegionPoolEntry/{}/{}/{}/'.format(self._zone, self._fqdn, - self._region_code) - api_args = {'address': self._address} + uri = '/GSLBRegionPoolEntry/{0}/{1}/{2}/'.format(self.zone, self.fqdn, + self.region_code) + api_args = {'address': self.address} if label: - api_args['label'] = self._label + api_args['label'] = label if weight: - if weight not in self.valid_weight: - raise DynectInvalidArgumentError('weight', weight, - self.valid_weight) - api_args['weight'] = self._weight + api_args['weight'] = weight if serve_mode: - if serve_mode not in self.valid_serve_modes: - raise DynectInvalidArgumentError('serve_mode', serve_mode, - self.valid_serve_modes) - api_args['serve_mode'] = self._serve_mode + api_args['serve_mode'] = serve_mode response = DynectSession.get_session().execute(uri, 'POST', api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - def _get(self): - """Get an existing :class:`GSLBRegionPoolEntry` object from the DynECT - System - """ - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) + self._build(response['data']) - def _update(self, api_args): - """Private update method""" - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) + def _update(self, **api_args): + if 'address' in api_args: + api_args['new_address'] = api_args.pop('address') + super(GSLBRegionPoolEntry, self)._update(**api_args) def sync(self): """Sync this :class:`GSLBRegionPoolEntry` object with the DynECT System """ self._get() - @property - def zone(self): - """Zone monitored by this :class:`GSLBRegionPoolEntry`""" - return self._zone - @zone.setter - def zone(self, value): - pass - - @property - def fqdn(self): - """The fqdn of the specific node which will be monitored by this - :class:`GSLBRegionPoolEntry` - """ - return self._fqdn - @fqdn.setter - def fqdn(self, value): - pass - - @property - def region_code(self): - """ISO Region Code for this :class:`GSLBRegionPoolEntry`""" - return self._region_code - @region_code.setter - def region_code(self, value): - pass - - @property - def address(self): - """The IP address or FQDN of this Node IP""" - return self._address - @address.setter - def address(self, value): - self._address = value - api_args = {'new_address': self._address} - self._update(api_args) - - @property - def label(self): - """Identifying descriptive information for this - :class:`GSLBRegionPoolEntry` - """ - return self._label - @label.setter - def label(self, value): - self._label = value - api_args = {'label': self._label} - self._update(api_args) - - @property - def weight(self): - """A number in the range of 1-14 controlling the order in which this - :class:`GSLBRegionPoolEntry` will be served. - """ - return self._weight - @weight.setter - def weight(self, new_weight): - if new_weight not in self.valid_weight: - raise DynectInvalidArgumentError(new_weight, - self.valid_weight) - self._weight = new_weight - api_args = {'weight': self._weight} - self._update(api_args) - - @property - def serve_mode(self): - """Sets the behavior of this particular record. Must be one of 'always', - 'obey', 'remove', or 'no' - """ - return self._serve_mode - @serve_mode.setter - def serve_mode(self, new_serve_mode): - if new_serve_mode not in self.valid_serve_modes: - raise DynectInvalidArgumentError('serve_mode', new_serve_mode, - self.valid_serve_modes) - self._serve_mode = new_serve_mode - api_args = {'serve_mode': self._serve_mode} - self._update(api_args) - def to_json(self): """Convert this object into a json blob""" - output = {'address': self._address} - if self._label: - output['label'] = self._label - if self._weight: - output['weight'] = self._weight - if self._serve_mode: - output['serve_mode'] = self._serve_mode + output = {'address': self.address} + if self.label: + output['label'] = self.label + if self.weight: + output['weight'] = self.weight + if self.serve_mode: + output['serve_mode'] = self.serve_mode return output - def delete(self): - """Delete this :class:`GSLBRegionPoolEntry` from the DynECT System""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) - def __str__(self): """str override""" - s = force_unicode(': {}') - return s.format(self._region_code) + return force_unicode(': {0}'.format( + self.region_code) + ) __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -392,8 +187,22 @@ def __bytes__(self): return bytes(self.__str__()) -class GSLBRegion(object): +class GSLBRegion(APIObject): """docstring for GSLBRegion""" + uri = '/GSLBRegion/{zone}/{fqdn}/{region}/' + session_type = DynectSession + zone = ImmutableAttribute('zone') + fqdn = ImmutableAttribute('fqdn') + region_code = ImmutableAttribute('region_code') + pool = ListAttribute('pool') + serve_count = IntegerAttribute('serve_count') + failover_mode = ValidatedAttribute('failover_mode', + validator=('ip', 'cname', 'region', + 'global')) + failover_data = ValidatedAttribute('failover_data', + validator=('ip', 'cname', 'region', + 'global')) + def __init__(self, zone, fqdn, region_code, *args, **kwargs): """Create a :class:`GSLBRegion` object @@ -406,42 +215,12 @@ def __init__(self, zone, fqdn, region_code, *args, **kwargs): response :param failover_mode: How the :class:`GSLBRegion` should failover. Must be one of 'ip', 'cname', 'region', 'global' - :param failover_data: Dependent upon failover_mode. Must be one of 'ip', - 'cname', 'region', 'global' + :param failover_data: Dependent upon failover_mode. Must be one of + 'ip', 'cname', 'region', 'global' """ - super(GSLBRegion, self).__init__() - self._zone = zone - self._fqdn = fqdn - self._region_code = region_code - self._pool = self._serve_count = self._failover_mode = None - self._failover_data = None - self.uri = '/GSLBRegion/{}/{}/{}/'.format(self._zone, self._fqdn, - self._region_code) self._pool = [] - if len(args) == 0 and len(kwargs) == 0: - self._get() - if len(kwargs) > 0: - for key, val in kwargs.items(): - if key == 'pool': - for pool in val: - if isinstance(pool, dict): - self._pool.append(GSLBRegionPoolEntry(self._zone, - self._fqdn, - self._region_code, - **pool)) - else: - self._pool.append(pool) - else: - setattr(self, '_' + key, val) - elif len(args) > 0: - for pool in args[0]: - if isinstance(pool, dict): - self._pool.append(GSLBRegionPoolEntry(self._zone, - self._fqdn, - self._region_code, - **pool)) - else: - self._pool.append(pool) + self.uri = self.uri.format(zone=zone, fqdn=fqdn, region=region_code) + super(GSLBRegion, self).__init__(*args, **kwargs) def _post(self, pool, serve_count=None, failover_mode=None, failover_data=None): @@ -450,7 +229,7 @@ def _post(self, pool, serve_count=None, failover_mode=None, self._serve_count = serve_count self._failover_mode = failover_mode self._failover_data = failover_data - uri = '/GSLBRegion/{}/{}/'.format(self._zone, self._fqdn) + uri = '/GSLBRegion/{0}/{1}/'.format(self.zone, self.fqdn) api_args = {'pool': self._pool.to_json()} if serve_count: api_args['serve_count'] = self._serve_count @@ -459,128 +238,29 @@ def _post(self, pool, serve_count=None, failover_mode=None, if failover_data: api_args['failover_data'] = self._failover_data response = DynectSession.get_session()(uri, 'POST', api_args) - for key, val in response['data'].items(): - if key == 'pool': - for pool in val: - if isinstance(pool, dict): - self._pool.append(GSLBRegionPoolEntry(self._zone, - self._fqdn, - self._region_code, - **pool)) - else: - setattr(self, '_' + key, val) + self._build(response['data']) - def _get(self): - """Get an existing :class:`GSLBRegion` object""" - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - for key, val in response['data'].items(): - if key == 'pool': - for pool in val: - if isinstance(pool, dict): - self._pool.append(GSLBRegionPoolEntry(self._zone, - self._fqdn, - self._region_code, - method=None, - **pool)) - else: - self._pool.append(pool) - else: - setattr(self, '_' + key, val) + def _build(self, data): + if 'pool' in data: + pools = data.pop('pool') + for pool in pools: + if isinstance(pool, dict): + self._pool.append(GSLBRegionPoolEntry(self.zone, self.fqdn, + self.region_code, + method=None, **pool)) + else: + self._pool.append(pool) + super(GSLBRegion, self)._build(data) - def _update(self, api_args): - """Private udpate method for PUT commands""" - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key == 'pool': - for pool in val: - if isinstance(pool, dict): - self._pool.append(GSLBRegionPoolEntry(self._zone, - self._fqdn, - self._region_code, - method=None, - **pool)) - else: - self._pool.append(pool) - else: - setattr(self, '_' + key, val) + def _update(self, **api_args): + if 'pool' in api_args: + api_args['pool'] = api_args['pool'].to_json() + super(GSLBRegion, self)._update(**api_args) def sync(self): """Sync this :class:`GSLBRegion` object with the DynECT System""" self._get() - @property - def zone(self): - """Zone monitored by this :class:`GSLBRegion`""" - return self._zone - @zone.setter - def zone(self, value): - pass - - @property - def fqdn(self): - """The fqdn of the specific node which will be monitored by this - :class:`GSLBRegion` - """ - return self._fqdn - @fqdn.setter - def fqdn(self, value): - pass - - @property - def region_code(self): - """ISO region code of this :class:`GSLBRegion`""" - return self._region_code - @region_code.setter - def region_code(self, value): - pass - - @property - def serve_count(self): - """How many records will be returned in each DNS response""" - return self._serve_count - @serve_count.setter - def serve_count(self, value): - self._serve_count = value - api_args = {'serve_count': self._serve_count} - self._update(api_args) - - @property - def failover_mode(self): - """How the :class:`GSLBRegion` should failover. Must be one of 'ip', - 'cname', 'region', 'global' - """ - return self._failover_mode - @failover_mode.setter - def failover_mode(self, value): - self._failover_mode = value - api_args = {'failover_mode': self._failover_mode} - self._update(api_args) - - @property - def failover_data(self): - """Dependent upon failover_mode. Must be one of 'ip', 'cname', 'region', - 'global' - """ - return self._failover_data - @failover_data.setter - def failover_data(self, value): - self._failover_data = value - api_args = {'failover_data': self._failover_data} - self._update(api_args) - - @property - def pool(self): - """The IP Pool list for this :class:`GSLBRegion`""" - return self._pool - @pool.setter - def pool(self, value): - self._pool = value - api_args = {'pool': self._pool.to_json()} - self._update(api_args) - @property def _json(self): """Convert this :class:`GSLBRegion` to a json blob""" @@ -594,14 +274,9 @@ def _json(self): output['failover_data'] = self._failover_data return output - def delete(self): - """Delete this :class:`GSLBRegion`""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) - def __str__(self): """str override""" - return force_unicode(': {}').format(self._region_code) + return force_unicode(': {0}').format(self.region_code) __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -609,8 +284,32 @@ def __bytes__(self): return bytes(self.__str__()) -class GSLB(object): +class GSLB(APIObject): """A Global Server Load Balancing (GSLB) service""" + uri = '/GSLB/{zone}/{fqdn}/' + session_type = DynectSession + zone = ImmutableAttribute('zone') + fqdn = ImmutableAttribute('fqdn') + auto_recover = ValidatedAttribute('auto_recover', validator=('Y', 'N')) + ttl = ValidatedAttribute('ttl', validator=(30, 60, 150, 300, 450)) + notify_events = ValidatedAttribute('notify_events', + validator=('ip', 'svc', 'nosrv')) + syslog_server = StringAttribute('syslog_server') + syslog_port = IntegerAttribute('syslog_port') + syslog_ident = StringAttribute('syslog_ident') + syslog_facility = ValidatedAttribute('syslog_facility', + validator=( + 'kern', 'user', 'mail', 'daemon', + 'auth', 'syslog', 'lpr', 'news', + 'uucp', 'cron', 'authpriv', 'ftp', + 'ntp', 'security', 'console', + 'local0', 'local1', 'local2', + 'local3', 'local4', 'local5', + 'local6', 'local7' + )) + monitor = ClassAttribute('monitor', class_type=Monitor) + contact_nickname = StringAttribute('contact_nickname') + def __init__(self, zone, fqdn, *args, **kwargs): """Create a :class:`GSLB` object @@ -637,79 +336,42 @@ def __init__(self, zone, fqdn, *args, **kwargs): :param monitor: The health :class:`Monitor` for this service :param contact_nickname: Name of contact to receive notifications """ - super(GSLB, self).__init__() - self.valid_auto_recover = ('Y', 'N') - self.valid_ttls = (30, 60, 150, 300, 450) - self.valid_notify_events = ('ip', 'svc', 'nosrv') - self.valid_syslog_facility = ('kern', 'user', 'mail', 'daemon', 'auth', - 'syslog', 'lpr', 'news', 'uucp', 'cron', - 'authpriv', 'ftp', 'ntp', 'security', - 'console', 'local0', 'local1', 'local2', - 'local3', 'local4', 'local5', 'local6', - 'local7') - self._zone = zone - self._fqdn = fqdn - self.uri = '/GSLB/{}/{}/'.format(self._zone, self._fqdn) - self._auto_recover = self._ttl = self._notify_events = None - self._syslog_server = self._syslog_port = self._syslog_ident = None - self._syslog_facility = self._monitor = self._contact_nickname = None + self.uri = self.uri.format(zone=zone, fqdn=fqdn) + + super(GSLB, self).__init__(*args, **kwargs) + self._active = self._status = self.active = None self._region = APIList(DynectSession.get_session, 'region') - if 'api' in kwargs: - del kwargs['api'] - self._build(kwargs) - elif len(args) == 0 and len(kwargs) == 0: - self._get() - else: - self._post(*args, **kwargs) + self._region.uri = self.uri def _post(self, contact_nickname, region, auto_recover=None, ttl=None, notify_events=None, syslog_server=None, syslog_port=514, syslog_ident='dynect', syslog_facility='daemon', monitor=None): """Create a new :class:`GSLB` service object on the DynECT System""" - self._auto_recover = auto_recover - self._ttl = ttl - self._notify_events = notify_events - self._syslog_server = syslog_server - self._syslog_port = syslog_port - self._syslog_ident = syslog_ident - self._syslog_facility = syslog_facility - self._region += region - self._monitor = monitor - self._contact_nickname = contact_nickname - api_args = {'contact_nickname': self._contact_nickname, - 'region': [r._json for r in self._region]} + api_args = {'contact_nickname': contact_nickname, + 'region': [r._json for r in region]} if auto_recover: - api_args['auto_recover'] = self._auto_recover + api_args['auto_recover'] = auto_recover if ttl: - api_args['ttl'] = self._ttl + api_args['ttl'] = ttl if notify_events: - api_args['notify_events'] = self._notify_events + api_args['notify_events'] = notify_events if syslog_server: - api_args['syslog_server'] = self._syslog_server + api_args['syslog_server'] = syslog_server if syslog_port: - api_args['syslog_port'] = self._syslog_port + api_args['syslog_port'] = syslog_port if syslog_ident: - api_args['syslog_ident'] = self._syslog_ident + api_args['syslog_ident'] = syslog_ident if syslog_facility: - api_args['syslog_facility'] = self._syslog_facility + api_args['syslog_facility'] = syslog_facility if monitor: - api_args['monitor'] = self._monitor.to_json() - self._monitor.zone = self._zone - self._monitor.fqdn = self._fqdn + api_args['monitor'] = monitor.to_json() response = DynectSession.get_session().execute(self.uri, 'POST', api_args) self._build(response['data']) - def _get(self): - """Get an existing :class:`GSLB` service object""" - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - self._build(response['data']) - - def _build(self, data, region=True): + def _build(self, data): """Private method which builds the objects fields based on the data returned by an API call @@ -717,20 +379,38 @@ def _build(self, data, region=True): :param region: Boolean flag specifying whether to rebuild the region objects or not """ - for key, val in data.items(): - if key == 'region': - if region: - self._region = APIList(DynectSession.get_session, 'region') - for region in val: - region_code = region.pop('region_code', None) - self._region.append(GSLBRegion(self._zone, self._fqdn, - region_code, **region)) - elif key == 'monitor': + if 'region' in data: + self._region = APIList(DynectSession.get_session, 'region') + for region in data.pop('region'): + region_code = region.pop('region_code', None) + self._region.append(GSLBRegion(self.zone, self.fqdn, + region_code, **region)) + self._region.uri = self.uri + if 'monitor' in data: + if getattr(self, '_monitor', None) is not None: # We already have the monitor object, no need to rebuild it - pass + data.pop('monitor') else: - setattr(self, '_' + key, val) - self._region.uri = self.uri + self._monitor = data.pop('monitor') + self._monitor.zone = self.zone + self._monitor.fqdn = self.fqdn + super(GSLB, self)._build(data) + + def _update(self, **api_args): + if 'region' in api_args: + region = api_args.pop('region') + if isinstance(region, list) and not isinstance(region, APIList): + self._region = APIList(DynectSession.get_session, 'region', + None, region) + elif isinstance(region, APIList): + self._region = region + self._region.uri = self.uri + if 'monitor' in api_args: + monitor = api_args.pop('monitor') + # We're only going accept new monitors of type Monitor + if isinstance(monitor, Monitor): + api_args['monitor'] = monitor.to_json() + super(GSLB, self)._update(**api_args) def sync(self): """Sync this :class:`GSLB` object with the DynECT System""" @@ -738,17 +418,11 @@ def sync(self): def activate(self): """Activate this :class:`GSLB` service on the DynECT System""" - api_args = {'activate': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data'], region=False) + self._update(activate=True) def deactivate(self): """Deactivate this :class:`GSLB` service on the DynECT System""" - api_args = {'deactivate': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data'], region=False) + self._update(deactivate=True) def recover(self, address=None): """Recover the GSLB service on the designated zone node or a specific @@ -762,39 +436,7 @@ def recover(self, address=None): api_args['recover'] = True response = DynectSession.get_session().execute(self.uri, 'PUT', api_args) - self._build(response['data'], region=False) - - @property - def auto_recover(self): - """Indicates whether or not the service should automatically come out of - failover when the IP addresses resume active status or if the service - should remain in failover until manually reset. Must be 'Y' or 'N' - """ - return self._auto_recover - @auto_recover.setter - def auto_recover(self, value): - if value not in self.valid_auto_recover: - raise DynectInvalidArgumentError('auto_recover', value, - self.valid_auto_recover) - self._auto_recover = value - api_args = {'auto_recover': self._auto_recover} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data'], region=False) - - @property - def status(self): - """The current state of the service. Will be one of 'unk', 'ok', - 'trouble', or 'failover' - """ - api_args = {} - respnose = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - self._status = respnose['data']['status'] - return self._status - @status.setter - def status(self, value): - pass + self._build(response['data']) @property def active(self): @@ -817,147 +459,9 @@ def active(self, value): elif value in activate and not self.active: self.activate() - @property - def ttl(self): - """Time To Live in seconds of records in the service. Must be less than - 1/2 of the Health Probe's monitoring interval. Must be one of 30, 60, - 150, 300, or 450 - """ - return self._ttl - @ttl.setter - def ttl(self, value): - if value not in self.valid_ttls: - raise DynectInvalidArgumentError('ttl', value, self.valid_ttls) - self._ttl = value - api_args = {'ttl': self._ttl} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data'], region=False) - - @property - def notify_events(self): - """A comma separated list of the events which trigger notifications. - Must be one of 'ip', 'svc', or 'nosrv' - """ - return self._notify_events - @notify_events.setter - def notify_events(self, value): - if value not in self.valid_notify_events: - raise DynectInvalidArgumentError('notify_events', value, - self.valid_notify_events) - self._notify_events = value - api_args = {'notify_events': self._notify_events} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data'], region=False) - - @property - def syslog_server(self): - """The Hostname or IP address of a server to receive syslog - notifications on monitoring events - """ - return self._syslog_server - @syslog_server.setter - def syslog_server(self, value): - self._syslog_server = value - api_args = {'syslog_server': self._syslog_server} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data'], region=False) - - @property - def syslog_port(self): - """The port where the remote syslog server listens for notifications""" - return self._syslog_port - @syslog_port.setter - def syslog_port(self, value): - self._syslog_port = value - api_args = {'syslog_port': self._syslog_port} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data'], region=False) - - @property - def syslog_ident(self): - """The ident to use when sending syslog notifications""" - return self._syslog_ident - @syslog_ident.setter - def syslog_ident(self, value): - self._syslog_ident = value - api_args = {'syslog_ident': self._syslog_ident} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data'], region=False) - - @property - def syslog_facility(self): - """The syslog facility to use when sending syslog notifications. Must be - one of 'kern', 'user', 'mail', 'daemon', 'auth', 'syslog', 'lpr', - 'news', 'uucp', 'cron', 'authpriv', 'ftp', 'ntp', 'security', 'console', - 'local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', or - 'local7' - """ - return self._syslog_facility - @syslog_facility.setter - def syslog_facility(self, value): - if value not in self.valid_syslog_facility: - raise DynectInvalidArgumentError('syslog_facility', value, - self.valid_syslog_facility) - self._syslog_facility = value - api_args = {'syslog_facility': self._syslog_facility} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data'], region=False) - - @property - def region(self): - """A list of :class:`GSLBRegion`'s""" - return self._region - @region.setter - def region(self, value): - if isinstance(value, list) and not isinstance(value, APIList): - self._region = APIList(DynectSession.get_session, 'region', None, - value) - elif isinstance(value, APIList): - self._region = value - self._region.uri = self.uri - - @property - def monitor(self): - """The health :class:`Monitor` for this service""" - return self._monitor - @monitor.setter - def monitor(self, value): - # We're only going accept new monitors of type Monitor - if isinstance(value, Monitor): - api_args = {'monitor': value.to_json()} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data'], region=False) - self._monitor = value - - @property - def contact_nickname(self): - """Name of contact to receive notifications from this :class:`GSLB` - service - """ - return self._contact_nickname - @contact_nickname.setter - def contact_nickname(self, value): - self._contact_nickname = value - api_args = {'contact_nickname': self._contact_nickname} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data'], region=False) - - def delete(self): - """Delete this :class:`GSLB` service from the DynECT System""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) - def __str__(self): """str override""" - return force_unicode(': {}').format(self._fqdn) + return force_unicode(': {0}').format(self.fqdn) __repr__ = __unicode__ = __str__ def __bytes__(self): From 4800bbc1f9806b7a85fbcbaa809da45f9fb60afa Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 13 Nov 2014 22:57:53 -0500 Subject: [PATCH 10/59] Updated gslb.Monitor to be a services._shared.BaseMonitor type --- dyn/tm/services/gslb.py | 95 +++-------------------------------------- 1 file changed, 5 insertions(+), 90 deletions(-) diff --git a/dyn/tm/services/gslb.py b/dyn/tm/services/gslb.py index e44a9c8..cf15b77 100644 --- a/dyn/tm/services/gslb.py +++ b/dyn/tm/services/gslb.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from ._shared import BaseMonitor from ..utils import APIList from ..session import DynectSession from ...core import (APIObject, ImmutableAttribute, StringAttribute, @@ -10,76 +11,8 @@ __all__ = ['Monitor', 'GSLBRegionPoolEntry', 'GSLBRegion', 'GSLB'] -class Monitor(APIObject): - """A :class:`Monitor` for a GSLB Service""" - session_type = DynectSession - protocol = ValidatedAttribute('protocol', - validator=('HTTP', 'HTTPS', 'PING', 'SMTP', - 'TCP')) - interval = ValidatedAttribute('interval', validator=(1, 5, 10, 15)) - retries = IntegerAttribute('retries') - timeout = ValidatedAttribute('timeout', validator=(10, 15, 25, 30)) - port = IntegerAttribute('port') - path = StringAttribute('path') - host = StringAttribute('host') - header = StringAttribute('header') - expected = StringAttribute('expected') - - def __init__(self, protocol, interval, retries=None, timeout=None, - port=None, path=None, host=None, header=None, expected=None): - """Create a :class:`Monitor` object - - :param protocol: The protocol to monitor. Must be either HTTP, HTTPS, - PING, SMTP, or TCP - :param interval: How often (in minutes) to run the monitor. Must be 1, - 5, 10, or 15, - :param retries: The number of retries the monitor attempts on failure - before giving up - :param timeout: The amount of time in seconds before the connection - attempt times out - :param port: For HTTP(S)/SMTP/TCP probes, an alternate connection port - :param path: For HTTP(S) probes, a specific path to request - :param host: For HTTP(S) probes, a value to pass in to the Host - :param header: For HTTP(S) probes, additional header fields/values to - pass in, separated by a newline character. - :param expected: For HTTP(S) probes, a string to search for in the - response. For SMTP probes, a string to compare the banner against. - Failure to find this string means the monitor will report a down - status. - """ - super(Monitor, self).__init__() - self._protocol = protocol - self._interval = interval - self._retries = retries - self._timeout = timeout - self._port = port - self._path = path - self._host = host - self._header = header - self._expected = expected - self.zone = None - self.fqdn = None - - def _post(self, *args, **kwargs): - """You can't create a HealthMonitor on it's own, so force _post and - _get to be no-ops - """ - pass - _get = _post - - def _update(self, **api_args): - mon_args = {'monitor': api_args} - super(Monitor, self)._update(**mon_args) - - def to_json(self): - """Convert this :class:`HealthMonitor` object to a JSON blob""" - json_blob = {'protocol': self.protocol, - 'interval': self.interval} - for key, val in self.__dict__.items(): - if val is not None and not hasattr(val, '__call__') and \ - key.startswith('_'): - json_blob[key[1:]] = val - return json_blob +class GSLBMonitor(BaseMonitor): + """A :class:`GSLBMonitor` for a GSLB Service""" @property def uri(self): @@ -87,24 +20,6 @@ def uri(self): return '/GSLB/{0}/{1}/'.format(self.zone, self.fqdn) raise ValueError - @property - def status(self): - """Get the current status of this :class:`Monitor` from the - DynECT System - """ - uri = '/Failover/{}/{}/'.format(self.zone, self.fqdn) - respnose = DynectSession.get_session().execute(uri, 'GET') - return respnose['data']['status'] - - def __str__(self): - """str override""" - return force_unicode(': {0}').format(self.protocol) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) - class GSLBRegionPoolEntry(APIObject): """:class:`GSLBRegionPoolEntry`""" @@ -307,7 +222,7 @@ class GSLB(APIObject): 'local3', 'local4', 'local5', 'local6', 'local7' )) - monitor = ClassAttribute('monitor', class_type=Monitor) + monitor = ClassAttribute('monitor', class_type=GSLBMonitor) contact_nickname = StringAttribute('contact_nickname') def __init__(self, zone, fqdn, *args, **kwargs): @@ -408,7 +323,7 @@ def _update(self, **api_args): if 'monitor' in api_args: monitor = api_args.pop('monitor') # We're only going accept new monitors of type Monitor - if isinstance(monitor, Monitor): + if isinstance(monitor, GSLBMonitor): api_args['monitor'] = monitor.to_json() super(GSLB, self)._update(**api_args) From aaffb2627014276f49d437c10c38b64a76f2a4c0 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 13 Nov 2014 22:58:14 -0500 Subject: [PATCH 11/59] Adding BaseMonitor implementation --- dyn/tm/services/_shared.py | 106 +++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 dyn/tm/services/_shared.py diff --git a/dyn/tm/services/_shared.py b/dyn/tm/services/_shared.py new file mode 100644 index 0000000..145ab35 --- /dev/null +++ b/dyn/tm/services/_shared.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +from ..session import DynectSession +from ...core import (APIObject, StringAttribute, ValidatedAttribute, + IntegerAttribute) +from ...compat import force_unicode + +__author__ = 'jnappi' + + +class BaseMonitor(APIObject): + """A :class:`Monitor` for a GSLB Service""" + session_type = DynectSession + protocol = ValidatedAttribute('protocol', + validator=('HTTP', 'HTTPS', 'PING', 'SMTP', + 'TCP')) + interval = ValidatedAttribute('interval', validator=(1, 5, 10, 15)) + retries = IntegerAttribute('retries') + timeout = ValidatedAttribute('timeout', validator=(10, 15, 25, 30)) + port = IntegerAttribute('port') + path = StringAttribute('path') + host = StringAttribute('host') + header = StringAttribute('header') + expected = StringAttribute('expected') + + def __init__(self, protocol, interval, retries=None, timeout=None, + port=None, path=None, host=None, header=None, expected=None): + """Create a :class:`Monitor` object + + :param protocol: The protocol to monitor. Must be either HTTP, HTTPS, + PING, SMTP, or TCP + :param interval: How often (in minutes) to run the monitor. Must be 1, + 5, 10, or 15, + :param retries: The number of retries the monitor attempts on failure + before giving up + :param timeout: The amount of time in seconds before the connection + attempt times out + :param port: For HTTP(S)/SMTP/TCP probes, an alternate connection port + :param path: For HTTP(S) probes, a specific path to request + :param host: For HTTP(S) probes, a value to pass in to the Host + :param header: For HTTP(S) probes, additional header fields/values to + pass in, separated by a newline character. + :param expected: For HTTP(S) probes, a string to search for in the + response. For SMTP probes, a string to compare the banner against. + Failure to find this string means the monitor will report a down + status. + """ + super(BaseMonitor, self).__init__() + self._protocol = protocol + self._interval = interval + self._retries = retries + self._timeout = timeout + self._port = port + self._path = path + self._host = host + self._header = header + self._expected = expected + self.zone = None + self.fqdn = None + + def _post(self, *args, **kwargs): + """You can't create a HealthMonitor on it's own, so force _post and + _get to be no-ops + """ + pass + _get = _post + + def _update(self, **api_args): + mon_args = {'monitor': api_args} + super(BaseMonitor, self)._update(**mon_args) + + def _build(self, data): + super(BaseMonitor, self)._build(data.pop('monitor')) + + def to_json(self): + """Convert this :class:`HealthMonitor` object to a JSON blob""" + json_blob = {'protocol': self.protocol, + 'interval': self.interval} + for key, val in self.__dict__.items(): + if val is not None and not hasattr(val, '__call__') and \ + key.startswith('_'): + json_blob[key[1:]] = val + return json_blob + + @property + def uri(self): + if self.zone is not None and self.fqdn is not None: + return '/GSLB/{0}/{1}/'.format(self.zone, self.fqdn) + raise ValueError + + @property + def status(self): + """Get the current status of this :class:`Monitor` from the + DynECT System + """ + respnose = DynectSession.get_session().execute(self.uri, 'GET') + return respnose['data']['status'] + + def __str__(self): + """str override""" + return force_unicode('<{0}>: {1}').format(self.__class__.__name__, + self.protocol) + __repr__ = __unicode__ = __str__ + + def __bytes__(self): + """bytes override""" + return bytes(self.__str__()) From 18672ad5eb9da75010b943354622307e9b1d8446 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 13 Nov 2014 23:00:11 -0500 Subject: [PATCH 12/59] Replaced afo's HealthMonitor with a BaseMonitor subclass named AFOMonitor --- dyn/tm/services/active_failover.py | 99 +++--------------------------- 1 file changed, 7 insertions(+), 92 deletions(-) diff --git a/dyn/tm/services/active_failover.py b/dyn/tm/services/active_failover.py index 7d23cda..eecca60 100644 --- a/dyn/tm/services/active_failover.py +++ b/dyn/tm/services/active_failover.py @@ -1,85 +1,17 @@ # -*- coding: utf-8 -*- +from ._shared import BaseMonitor from ..utils import Active from ..session import DynectSession from ...core import (APIObject, ImmutableAttribute, StringAttribute, - ClassAttribute, IntegerAttribute, - ValidatedListAttribute, ValidatedAttribute) + ClassAttribute, IntegerAttribute, ValidatedListAttribute) from ...compat import force_unicode __author__ = 'jnappi' -__all__ = ['HealthMonitor', 'ActiveFailover'] +__all__ = ['AFOMonitor', 'ActiveFailover'] -class HealthMonitor(APIObject): +class AFOMonitor(BaseMonitor): """A health monitor for an :class:`ActiveFailover` service""" - session_type = DynectSession - protocol = ValidatedAttribute('protocol', - validator=('HTTP', 'HTTPS', 'PING', 'SMTP', - 'TCP')) - interval = ValidatedAttribute('interval', validator=(1, 5, 10, 15)) - retries = IntegerAttribute('retries') - timeout = ValidatedAttribute('timeout', validator=(10, 15, 25, 30)) - port = IntegerAttribute('port') - path = StringAttribute('path') - host = StringAttribute('host') - header = StringAttribute('header') - expected = StringAttribute('expected') - - def __init__(self, protocol, interval, retries=None, timeout=None, - port=None, path=None, host=None, header=None, expected=None): - """Create a :class:`HealthMonitor` object - - :param protocol: The protocol to monitor. Must be either HTTP, HTTPS, - PING, SMTP, or TCP - :param interval: How often (in minutes) to run this - :class:`HealthMonitor`. Must be 1, 5, 10, or 15, - :param retries: The number of retries the monitor attempts on failure - before giving up - :param timeout: The amount of time in seconds before the connection - attempt times out - :param port: For HTTP(S)/SMTP/TCP probes, an alternate connection port - :param path: For HTTP(S) probes, a specific path to request - :param host: For HTTP(S) probes, a value to pass in to the Host - :param header: For HTTP(S) probes, additional header fields/values to - pass in, separated by a newline character. - :param expected: For HTTP(S) probes, a string to search for in the - response. For SMTP probes, a string to compare the banner against. - Failure to find this string means the monitor will report a down - status. - """ - super(HealthMonitor, self).__init__() - self._protocol = protocol - self._interval = interval - self._retries = retries - self._timeout = timeout - self._port = port - self._path = path - self._host = host - self._header = header - self._expected = expected - self.zone = None - self.fqdn = None - - def _post(self, *args, **kwargs): - """You can't create a HealthMonitor on it's own, so force _post and - _get to be no-ops - """ - pass - _get = _post - - def _update(self, **api_args): - mon_args = {'monitor': api_args} - super(HealthMonitor, self)._update(**mon_args) - - def to_json(self): - """Convert this :class:`HealthMonitor` object to a JSON blob""" - json_blob = {'protocol': self.protocol, - 'interval': self.interval} - for key, val in self.__dict__.items(): - if val is not None and not hasattr(val, '__call__') and \ - key.startswith('_'): - json_blob[key[1:]] = val - return json_blob @property def uri(self): @@ -87,23 +19,6 @@ def uri(self): return '/Failover/{0}/{1}/'.format(self.zone, self.fqdn) raise ValueError - @property - def status(self): - """Get the current status of this :class:`HealthMonitor` from the - DynECT System - """ - respnose = DynectSession.get_session().execute(self.uri, 'GET') - return respnose['data']['status'] - - def __str__(self): - """str override""" - return force_unicode(': {0}').format(self._protocol) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) - # noinspection PyUnresolvedReferences class ActiveFailover(APIObject): @@ -119,7 +34,7 @@ class ActiveFailover(APIObject): address = StringAttribute('address') failover_mode = StringAttribute('failover_mode') failover_data = StringAttribute('failover_data') - monitor = ClassAttribute('monitor', HealthMonitor) + monitor = ClassAttribute('monitor', AFOMonitor) contact_nickname = StringAttribute('contact_nickname') auto_recover = StringAttribute('auto_recover') notify_events = ValidatedListAttribute('notify_events', @@ -151,7 +66,7 @@ def __init__(self, zone, fqdn, *args, **kwargs): :param syslog_ident: The ident to use when sending syslog notifications :param syslog_facility: The syslog facility to use when sending syslog notifications - :param monitor: The :class:`HealthMonitor` for this + :param monitor: The :class:`AFOMonitor` for this :class:`ActiveFailover` service :param contact_nickname: Name of contact to receive notifications from this :class:`ActiveFailover` service @@ -198,7 +113,7 @@ def _update(self, **api_args): def _build(self, data): """Build this object from the data returned in an API response""" if 'monitor' in data: - self._monitor = HealthMonitor(**data.pop('monitor')) + self._monitor = AFOMonitor(**data.pop('monitor')) if 'active' in data: self._active = Active(data.pop('active')) super(ActiveFailover, self)._build(data) From c09404de964dfe9c43e283bdd127ada965a47866 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 13 Nov 2014 23:00:35 -0500 Subject: [PATCH 13/59] Updated exported monitor name --- dyn/tm/services/gslb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dyn/tm/services/gslb.py b/dyn/tm/services/gslb.py index cf15b77..99b5172 100644 --- a/dyn/tm/services/gslb.py +++ b/dyn/tm/services/gslb.py @@ -8,7 +8,7 @@ from ...compat import force_unicode __author__ = 'jnappi' -__all__ = ['Monitor', 'GSLBRegionPoolEntry', 'GSLBRegion', 'GSLB'] +__all__ = ['GSLBMonitor', 'GSLBRegionPoolEntry', 'GSLBRegion', 'GSLB'] class GSLBMonitor(BaseMonitor): From dd1943fe8c2e2c6bc3d1a588d3e4faf67b339ce3 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 13 Nov 2014 23:08:07 -0500 Subject: [PATCH 14/59] Initial pass at porting dyn.tm.services.rttm to use the new APIObject and descriptor patterns, still need to complete docs and testing --- dyn/tm/services/rttm.py | 1007 ++++++++------------------------------- 1 file changed, 211 insertions(+), 796 deletions(-) diff --git a/dyn/tm/services/rttm.py b/dyn/tm/services/rttm.py index 0e50e05..56d96b1 100644 --- a/dyn/tm/services/rttm.py +++ b/dyn/tm/services/rttm.py @@ -1,257 +1,56 @@ # -*- coding: utf-8 -*- -import logging from datetime import datetime +from ._shared import BaseMonitor from ..utils import APIList, Active, unix_date -from ..errors import DynectInvalidArgumentError from ..session import DynectSession +from ...core import (APIObject, ImmutableAttribute, StringAttribute, + ValidatedAttribute, IntegerAttribute, ClassAttribute, + ValidatedListAttribute, ListAttribute) from ...compat import force_unicode __author__ = 'jnappi' -__all__ = ['Monitor', 'PerformanceMonitor', 'RegionPoolEntry', 'RTTMRegion', - 'RTTM'] +__all__ = ['RTTMMonitor', 'RTTMPerformanceMonitor', 'RegionPoolEntry', + 'RTTMRegion', 'RTTM'] -class Monitor(object): - """A :class:`Monitor` for RTTM Service. May be used as a HealthMonitor""" - def __init__(self, protocol, interval, retries=None, timeout=None, - port=None, path=None, host=None, header=None, expected=None): - """Create a :class:`Monitor` object - - :param protocol: The protocol to monitor. Must be either HTTP, HTTPS, - PING, SMTP, or TCP - :param interval: How often (in minutes) to run the monitor. Must be 1, - 5, 10, or 15, - :param retries: The number of retries the monitor attempts on failure - before giving up - :param timeout: The amount of time in seconds before the connection - attempt times out - :param port: For HTTP(S)/SMTP/TCP probes, an alternate connection port - :param path: For HTTP(S) probes, a specific path to request - :param host: For HTTP(S) probes, a value to pass in to the Host - :param header: For HTTP(S) probes, additional header fields/values to - pass in, separated by a newline character. - :param expected: For HTTP(S) probes, a string to search for in the - response. For SMTP probes, a string to compare the banner against. - Failure to find this string means the monitor will report a down - status. - """ - super(Monitor, self).__init__() - self._protocol = protocol - self._interval = interval - self._retries = retries - self._timeout = timeout - self._port = port - self._path = path - self._host = host - self._header = header - self._expected = expected - self.zone = self.fqdn = self._status = None - self.valid_protocols = ('HTTP', 'HTTPS', 'PING', 'SMTP', 'TCP') - self.valid_intervals = (1, 5, 10, 15) - self.valid_timeouts = (10, 15, 25, 30) - - def to_json(self): - """Convert this :class:`HealthMonitor` object to a JSON blob""" - json_blob = {'protocol': self._protocol, - 'interval': self._interval} - for key, val in self.__dict__.items(): - if val is not None and not hasattr(val, '__call__') and \ - key.startswith('_'): - json_blob[key[1:]] = val - return json_blob - - def __eq__(self, other): - """eq override for comparing :class:`HealthMonitor` objects to JSON - response hashes or other :class:`DNSSECKey` instances - - :param other: the value to compare this :class:`HealthMonitor` to. Valid - input types: `dict`, :class:`HealthMonitor` - """ - if isinstance(other, dict): - return False - elif isinstance(other, Monitor): - return False - return False - - def _get(self): - """Update this :class:`Monitor` with data from the Dyn System""" - uri = '/RTTM/{}/{}/'.format(self.zone, self.fqdn) - api_args = {} - response = DynectSession.get_session().execute(uri, 'GET', api_args) - self._build(response['data']['monitor']) - - def _update(self, api_args): - """Update the Dyn System with data from this :class:`Monitor`""" - uri = '/RTTM/{}/{}/'.format(self.zone, self.fqdn) - response = DynectSession.get_session().execute(uri, 'PUT', api_args) - self._build(response['data']['monitor']) - - def _build(self, data): - """Update the fields in this :class:`PerformanceMonitor` with the data - from API calls. - - :param data: The 'data' field of API responses - """ - for key, val in data.items(): - setattr(self, '_' + key, val) +class RTTMMonitor(BaseMonitor): + """A :class:`RTTMMonitor` for RTTM Service.""" @property - def status(self): - """Get the current status of this :class:`HealthMonitor` from the DynECT - System - """ - self._get() - return self._status + def uri(self): + if self.zone is not None and self.fqdn is not None: + return '/RTTM/{0}/{1}/'.format(self.zone, self.fqdn) + raise ValueError - @property - def protocol(self): - """The protocol to monitor""" - return self._protocol - @protocol.setter - def protocol(self, value): - if value not in self.valid_protocols: - raise Exception - self._protocol = value - api_args = {'monitor': {'protocol': self._protocol}} - self._update(api_args) - - @property - def interval(self): - """How often to run this monitor""" - return self._interval - @interval.setter - def interval(self, value): - if value not in self.valid_intervals: - raise Exception - self._interval = value - api_args = {'monitor': {'interval': self._interval}} - self._update(api_args) - @property - def retries(self): - """The number of retries the monitor attempts on failure before giving - up - """ - return self._retries - @retries.setter - def retries(self, value): - self._retries = value - api_args = {'monitor': {'retries': self._retries}} - self._update(api_args) - - @property - def timeout(self): - """The amount of time in seconds before the connection attempt times - out - """ - return self._timeout - @timeout.setter - def timeout(self, value): - self._timeout = value - api_args = {'monitor': {'timeout': self._timeout}} - self._update(api_args) - - @property - def port(self): - """For HTTP(S)/SMTP/TCP probes, an alternate connection port""" - return self._port - @port.setter - def port(self, value): - self._port = value - api_args = {'monitor': {'port': self._port}} - self._update(api_args) - - @property - def path(self): - """For HTTP(S) probes, a specific path to request""" - return self._path - @path.setter - def path(self, value): - self._path = value - api_args = {'monitor': {'path': self._path}} - self._update(api_args) - - @property - def host(self): - """For HTTP(S) probes, a value to pass in to the Host""" - return self._host - @host.setter - def host(self, value): - self._host = value - api_args = {'monitor': {'host': self._host}} - self._update(api_args) - - @property - def header(self): - """For HTTP(S) probes, additional header fields/values to pass in, - separated by a newline character - """ - return self._header - @header.setter - def header(self, value): - self._header = value - api_args = {'monitor': {'header': self._header}} - self._update(api_args) - - @property - def expected(self): - """For HTTP(S) probes, a string to search for in the response. For - SMTP probes, a string to compare the banner against. Failure to find - this string means the monitor will report a down status - """ - return self._expected - @expected.setter - def expected(self, value): - self._expected = value - api_args = {'monitor': {'expected': self._expected}} - self._update(api_args) - - def __str__(self): - """str override""" - return force_unicode(': {}').format(self._protocol) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) - - -class PerformanceMonitor(Monitor): - """A :class:`PerformanceMonitor` for RTTM Service.""" +class RTTMPerformanceMonitor(RTTMMonitor): + """A :class:`RTTMPerformanceMonitor` for RTTM Service.""" def __init__(self, *args, **kwargs): - super(PerformanceMonitor, self).__init__(*args, **kwargs) + super(RTTMPerformanceMonitor, self).__init__(*args, **kwargs) self.valid_intervals = (10, 20, 30, 60) - def _get(self): - """Update this :class:`PerformanceMonitor` with data from the Dyn System - """ - uri = '/RTTM/{}/{}/'.format(self.zone, self.fqdn) - api_args = {} - response = DynectSession.get_session().execute(uri, 'GET', api_args) - self._build(response['data']['performance_monitor']) - - def _update(self, api_args): - """Update the Dyn System with data from this :class:`PerformanceMonitor` - """ - uri = '/RTTM/{}/{}/'.format(self.zone, self.fqdn) - response = DynectSession.get_session().execute(uri, 'PUT', api_args) - self._build(response['data']['performance_monitor']) - - def __str__(self): - """str override""" - return force_unicode(': {}').format(self._protocol) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) + def _build(self, data): + super(BaseMonitor, self)._build(data.pop('performance_monitor')) -class RegionPoolEntry(object): +class RegionPoolEntry(APIObject): """Creates a new RTTM service region pool entry in the zone/node indicated """ + session_type = DynectSession + + zone = ImmutableAttribute('zone') + fqdn = ImmutableAttribute('fqdn') + region_code = ImmutableAttribute('region_code') + address = StringAttribute('address') + label = StringAttribute('label') + weight = ValidatedAttribute('weight', validator=range(1, 16)) + serve_mode = ValidatedAttribute('serve_mode', + validator=('always', 'obey', 'remove', + 'no')) + logs = ImmutableAttribute('log') + def __init__(self, address, label, weight, serve_mode, **kwargs): """Create a :class:`RegionPoolEntry` object @@ -263,118 +62,29 @@ def __init__(self, address, label, weight, serve_mode, **kwargs): :param serve_mode: Sets the behavior of this particular record. Must be one of 'always', 'obey', 'remove', or 'no' """ - super(RegionPoolEntry, self).__init__() - self.valid_modes = ('always', 'obey', 'remove', 'no') - self._address = address - self._label = label - if weight not in range(1, 16): - raise DynectInvalidArgumentError('weight', weight, '1-15') - self._weight = weight - self._zone = self._fqdn = self._region_code = None - if serve_mode not in self.valid_modes: - raise DynectInvalidArgumentError('serve_mode', serve_mode, - self.valid_modes) - self._serve_mode = serve_mode - self._log = [] - for key, val in kwargs.items(): - setattr(self, key, val) + super(RegionPoolEntry, self).__init__(address, label, weight, + serve_mode, **kwargs) - def _update(self, args): - """Private method for processing various updates""" - uri = '/RTTMRegionPoolEntry/{}/{}/{}/{}/'.format(self._zone, self._fqdn, - self._region_code, - self._address) - response = DynectSession.get_session().execute(uri, 'PUT', args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) + @property + def uri(self): + uri = '/RTTMRegionPoolEntry/{zone}/{fqdn}/{region}/{address}/' + return uri.format(zone=self.zone, fqdn=self.fqdn, + region=self.region_code, address=self.address) def _get(self): - uri = '/RTTMRegionPoolEntry/{}/{}/{}/{}/'.format(self._zone, self._fqdn, - self._region_code, - self._address) args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - @property - def logs(self): - self._get() - return self._log - @logs.setter - def logs(self, value): - pass - - @property - def address(self): - """The IPv4 address or FQDN of this Node IP""" - return self._address - @address.setter - def address(self, new_address): - api_args = {'new_address': new_address} - self._update(api_args) - - @property - def label(self): - """A descriptive string identifying this IP""" - return self._label - @label.setter - def label(self, value): - self._label = value - api_args = {'label': self._label} - self._update(api_args) - - @property - def weight(self): - """A number from 1-15 describing how often this record should be served. - The higher the number, the more often the address is served - """ - return self._weight - @weight.setter - def weight(self, new_weight): - if new_weight < 1 or new_weight > 15: - raise DynectInvalidArgumentError('weight', new_weight, '1-15') - self._weight = new_weight - api_args = {'weight': self._weight} - self._update(api_args) - - @property - def serve_mode(self): - """Sets the behavior of this particular record""" - return self._serve_mode - @serve_mode.setter - def serve_mode(self, serve_mode): - if serve_mode not in self.valid_modes: - raise DynectInvalidArgumentError('serve_mode', serve_mode, - self.valid_modes) - self._serve_mode = serve_mode - api_args = {'serve_mode': self._serve_mode} - self._update(api_args) - - @property - def region_code(self): - """Name of the region""" - return self._region_code - @region_code.setter - def region_code(self, value): - pass + response = DynectSession.get_session().execute(self.uri, 'GET', args) + self._build(response['data']) def to_json(self): """Return a JSON representation of this RegionPoolEntry""" - json_blob = {'address': self._address, 'label': self._label, - 'weight': self._weight, 'serve_mode': self._serve_mode} + json_blob = {'address': self.address, 'label': self.label, + 'weight': self.weight, 'serve_mode': self.serve_mode} return json_blob - def delete(self): - """Delete this :class:`RegionPoolEntry`""" - uri = '/RTTMRegionPoolEntry/{}/{}/{}/{}/'.format(self._zone, self._fqdn, - self._region_code, - self._address) - DynectSession.get_session().execute(uri, 'DELETE', {}) - def __str__(self): """str override""" - return force_unicode(': {}').format(self._address) + return force_unicode(': {0}').format(self.address) __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -382,8 +92,30 @@ def __bytes__(self): return bytes(self.__str__()) -class RTTMRegion(object): +class RTTMRegion(APIObject): """docstring for RTTMRegion""" + uri = '/RTTMRegion/{zone}/{fqdn}/{region}/' + session_type = DynectSession + zone = ImmutableAttribute('zone') + fqdn = ImmutableAttribute('fqdn') + region_code = ValidatedAttribute('region_code', + validator=('US West', 'US Central', + 'US East', 'Asia', 'EU West', + 'EU Central', 'EU East', + 'global')) + autopopulate = StringAttribute('autopopulate') + ep = IntegerAttribute('ep') + apmc = IntegerAttribute('apmc') + epmc = IntegerAttribute('epmc') + serve_count = IntegerAttribute('serve_count') + failover_mode = ValidatedAttribute('failover_mode', + validator=('ip', 'cname', 'region', + 'global')) + failover_data = ValidatedAttribute('failover_data', + validator=('ip', 'cname', 'region', + 'global')) + pool = ListAttribute('pool') + def __init__(self, zone, fqdn, region_code, pool, autopopulate=None, ep=None, apmc=None, epmc=None, serve_count=None, failover_mode=None, failover_data=None): @@ -408,77 +140,42 @@ def __init__(self, zone, fqdn, region_code, pool, autopopulate=None, :param failover_data: Dependent upon failover_mode. Must be one of ip', 'cname', 'region', or 'global' """ - super(RTTMRegion, self).__init__() - self.valid_region_codes = ('US West', 'US Central', 'US East', 'Asia', - 'EU West', 'EU Central', 'EU East', 'global') - self.valid_modes = ('ip', 'cname', 'region', 'global') - self._zone = zone - self._fqdn = fqdn - if region_code not in self.valid_region_codes: - raise DynectInvalidArgumentError('region_code', region_code, - self.valid_region_codes) - self._region_code = region_code + self.uri = self.uri.format(zone=zone, fqdn=fqdn, region=region_code) if len(pool) > 0 and isinstance(pool[0], dict): - pool_list = pool - pool = [] - for item in pool_list: - rpe = RegionPoolEntry(**item) - rpe._zone = self._zone - rpe._fqdn = self._fqdn - rpe._region_code = self._region_code - pool.append(rpe) - self._pool = pool - self._autopopulate = autopopulate - self._ep = ep - self._apmc = apmc - self._epmc = epmc - self._serve_count = serve_count - self._failover_mode = failover_mode - self._failover_data = failover_data - self._status = None - self.uri = '/RTTMRegion/{}/{}/{}/'.format(self._zone, self._fqdn, - self._region_code) + self.pool = [] + for item in pool: + rpe = RegionPoolEntry(api=False, **item) + rpe.zone = self.zone + rpe.fqdn = self.fqdn + rpe.region_code = self.region_code + self.pool.append(rpe) + super(RTTMRegion, self).__init__(zone, fqdn, region_code, pool, + autopopulate, ep, apmc, epmc, + serve_count, failover_mode, + failover_data) def _post(self): """Create a new :class:`RTTMRegion` on the DynECT System""" - uri = '/RTTMRegion/{}/{}/'.format(self._zone, self._fqdn) - api_args = {'region_code': self._region_code, - 'pool': self._pool.to_json()} - if self._autopopulate: - if self._autopopulate not in ('Y', 'N'): - raise DynectInvalidArgumentError('autopopulate', - self._autopopulate, ('Y', 'N')) - api_args['autopopulate'] = self._autopopulate - if self._ep: - api_args['ep'] = self._ep - if self._apmc: - api_args['apmc'] = self._apmc - if self._epmc: - api_args['epmc'] = self._epmc - if self._serve_count: - api_args['serve_count'] = self._serve_count - if self._failover_mode: - if self._failover_mode not in self.valid_modes: - raise DynectInvalidArgumentError('failover_mode', - self._failover_mode, - self.valid_modes) - api_args['failover_mode'] = self._failover_mode - if self._failover_data: - if self._failover_data not in self.valid_modes: - raise DynectInvalidArgumentError('failover_data', - self._failover_data, - self.valid_modes) - api_args['failover_data'] = self._failover_data + uri = '/RTTMRegion/{0}/{1}/'.format(self.zone, self.fqdn) + api_args = {'region_code': self.region_code, + 'pool': self.pool.to_json()} + if self.autopopulate: + api_args['autopopulate'] = self.autopopulate + if self.ep: + api_args['ep'] = self.ep + if self.apmc: + api_args['apmc'] = self.apmc + if self.epmc: + api_args['epmc'] = self.epmc + if self.serve_count: + api_args['serve_count'] = self.serve_count + if self.failover_mode: + api_args['failover_mode'] = self.failover_mode + if self.failover_data: + api_args['failover_data'] = self.failover_data response = DynectSession.get_session().execute(uri, 'POST', api_args) self._build(response['data']) - def _get(self): - """Get an existing :class:`RTTMRegion` object from the DynECT System""" - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - self._build(response['data']) - def _update(self, api_args): """Private Update method to cut back on redundant code""" response = DynectSession.get_session().execute(self.uri, 'PUT', @@ -486,147 +183,33 @@ def _update(self, api_args): self._build(response['data']) def _build(self, data): - for key, val in data.items(): - if key == 'pool': - pass - else: - setattr(self, '_' + key, val) - - @property - def autopopulate(self): - """If set to Y, this region will automatically be filled in from the - global pool, and any other options passed in for this region will be - ignored. Must be either 'Y' or 'N'. - """ - return self._autopopulate - @autopopulate.setter - def autopopulate(self, value): - self._autopopulate = value - api_args = {'autopopulate': self._autopopulate} - self._update(api_args) - - @property - def ep(self): - """Eligibility Pool - How many records will make it into the eligibility - pool. The addresses that get chosen will be those that respond the - fastest - """ - return self._ep - @ep.setter - def ep(self, value): - self._ep = value - api_args = {'ep': self._ep} - self._update(api_args) - - @property - def apmc(self): - """The minimum amount of IPs that must be in the up state, otherwise the - region will be in failover. - """ - return self._apmc - @apmc.setter - def apmc(self, value): - self._apmc = value - api_args = {'apmc': self._apmc} - self._update(api_args) - - @property - def epmc(self): - """The minimum amount of IPs that must be populated in the EP, otherwise - the region will be in failover - """ - return self._epmc - @epmc.setter - def epmc(self, value): - self._epmc = value - api_args = {'epmc': self._epmc} - self._update(api_args) - - @property - def serve_count(self): - """How many records will be returned in each DNS response""" - return self._serve_count - @serve_count.setter - def serve_count(self, value): - self._serve_count = value - api_args = {'serve_count': self._serve_count} - self._update(api_args) - - @property - def failover_mode(self): - """How the region should failover. Must be one of 'ip', 'cname', - 'region', or 'global' - """ - return self._failover_mode - @failover_mode.setter - def failover_mode(self, value): - self._failover_mode = value - api_args = {'failover_mode': self._failover_mode} - self._update(api_args) - - @property - def failover_data(self): - """Dependent upon failover_mode. Must be one of ip', 'cname', 'region', - or 'global' - """ - return self._failover_data - @failover_data.setter - def failover_data(self, value): - if value not in self.valid_modes: - raise DynectInvalidArgumentError('failover_data', value, - self.valid_modes) - api_args = {'failover_data': value} - self._update(api_args) - - @property - def pool(self): - """The IP Pool list for this :class:`RTTMRegion`""" - return self._pool - @pool.setter - def pool(self, value): - self._pool = value - api_args = {'pool': self._pool} - self._update(api_args) - - @property - def status(self): - """The current state of the region.""" - self._get() - return self._status - @status.setter - def status(self, value): - pass + data.pop('pool', None) + super(RTTMRegion, self)._build(data) @property def _json(self): """Unpack this object and return it as a JSON blob""" - json_blob = {'region_code': self._region_code, - 'pool': [entry.to_json() for entry in self._pool]} - if self._autopopulate: - json_blob['autopopulate'] = self._autopopulate - if self._ep: - json_blob['ep'] = self._ep - if self._apmc: - json_blob['apmc'] = self._apmc - if self._epmc: - json_blob['epmc'] = self._epmc - if self._serve_count: - json_blob['serve_count'] = self._serve_count - if self._failover_mode: - json_blob['failover_mode'] = self._failover_mode - if self._failover_data: - json_blob['failover_data'] = self._failover_data + json_blob = {'region_code': self.region_code, + 'pool': [entry.to_json() for entry in self.pool]} + if self.autopopulate: + json_blob['autopopulate'] = self.autopopulate + if self.ep: + json_blob['ep'] = self.ep + if self.apmc: + json_blob['apmc'] = self.apmc + if self.epmc: + json_blob['epmc'] = self.epmc + if self.serve_count: + json_blob['serve_count'] = self.serve_count + if self.failover_mode: + json_blob['failover_mode'] = self.failover_mode + if self.failover_data: + json_blob['failover_data'] = self.failover_data return json_blob - def delete(self): - """Delete an existing :class:`RTTMRegion` object from the DynECT - System - """ - DynectSession.get_session().execute(self.uri, 'DELETE') - def __str__(self): """str override""" - return force_unicode(': {}').format(self._region_code) + return force_unicode(': {0}').format(self.region_code) __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -634,7 +217,33 @@ def __bytes__(self): return bytes(self.__str__()) -class RTTM(object): +class RTTM(APIObject): + uri = '/RTTM/{zone}/{fqdn}/' + session_type = DynectSession + zone = ImmutableAttribute('zone') + fqdn = ImmutableAttribute('fqdn') + auto_recover = ValidatedAttribute('auto_recover', validator=('Y', 'N')) + ttl = ValidatedAttribute('ttl', validator=(30, 60, 150, 300, 450)) + notify_events = ValidatedListAttribute('notify_events', + validator=('ip', 'svc', 'nosrv')) + syslog_server = StringAttribute('syslog_server') + syslog_port = IntegerAttribute('syslog_port') + syslog_ident = StringAttribute('syslog_ident') + syslog_facility = ValidatedAttribute('syslog_facility', + validator=( + 'kern', 'user', 'mail', 'daemon', + 'auth', 'syslog', 'lpr', 'news', + 'uucp', 'cron', 'authpriv', 'ftp', + 'ntp', 'security', 'console', + 'local0', 'local1', 'local2', + 'local3', 'local4', 'local5', + 'local6', 'local7' + )) + monitor = ClassAttribute('monitor', class_type=Monitor) + performance_monitor = ClassAttribute('performance_monitor', + class_type=PerformanceMonitor) + contact_nickname = StringAttribute('contact_nickname') + def __init__(self, zone, fqdn, *args, **kwargs): """Create a :class:`RTTM` object @@ -662,85 +271,44 @@ def __init__(self, zone, fqdn, *args, **kwargs): :param performance_monitor: The performance monitor for the service :param contact_nickname: Name of contact to receive notifications """ - super(RTTM, self).__init__() - self.valid_ttls = (30, 60, 150, 300, 450) - self.valid_notify_events = ('ip', 'svc', 'nosrv') - self.valid_syslog_facilities = ('kern', 'user', 'mail', 'daemon', - 'auth', 'syslog', 'lpr', 'news', 'uucp', - 'cron', 'authpriv', 'ftp', 'ntp', - 'security', 'console', 'local0', - 'local1', 'local2', 'local3', 'local4', - 'local5', 'local6', 'local7') - self._zone = zone - self._fqdn = fqdn - self.uri = '/RTTM/{}/{}/'.format(self._zone, self._fqdn) - self._auto_recover = self._ttl = self._notify_events = None - self._syslog_server = self._syslog_port = self._syslog_ident = None - self._syslog_facility = self._monitor = self._performance_monitor = None - self._contact_nickname = self._active = None - self._region = APIList(DynectSession.get_session, 'region') - if 'api' in kwargs: - del kwargs['api'] - self._build(kwargs) - elif len(args) == 0 and len(kwargs) == 0: - self._get() - else: - self._post(*args, **kwargs) - self._region.uri = self.uri + self.uri = self.uri.format(zone=zone, fqdn=fqdn) + super(RTTM, self).__init__(*args, **kwargs) + self.region.uri = self.uri def _post(self, contact_nickname, performance_monitor, region, ttl=None, auto_recover=None, notify_events=None, syslog_server=None, syslog_port=514, syslog_ident='dynect', syslog_facility='daemon', monitor=None): """Create a new RTTM Service on the DynECT System""" - self._auto_recover = auto_recover - self._ttl = ttl - self._notify_events = notify_events - self._syslog_server = syslog_server - self._syslog_port = syslog_port - self._syslog_ident = syslog_ident - self._syslog_facility = syslog_facility - self._region += region - self._monitor = monitor - self._performance_monitor = performance_monitor - self._contact_nickname = contact_nickname - api_args = {} + api_args = {'contact_nickname': contact_nickname, + 'performance_monitor': performance_monitor.to_json(), + 'region': region.to_json()} if auto_recover: - if auto_recover not in ('Y', 'N'): - raise DynectInvalidArgumentError('auto_recover', auto_recover, - ('Y', 'N')) - api_args['auto_recover'] = self._auto_recover + self.auto_recover = auto_recover + api_args['auto_recover'] = self.auto_recover if ttl: - if ttl not in self.valid_ttls: - raise DynectInvalidArgumentError('ttl', ttl, self.valid_ttls) - api_args['ttl'] = self._ttl + self.ttl = ttl + api_args['ttl'] = self.ttl if notify_events: - for event in notify_events: - if event not in self.valid_notify_events: - raise DynectInvalidArgumentError('notify_events', event, - self.valid_notify_events) - api_args['notify_events'] = self._notify_events + self.notify_events = notify_events + api_args['notify_events'] = self.notify_events if syslog_server: - api_args['syslog_server'] = self._syslog_server + api_args['syslog_server'] = syslog_server if syslog_port: - api_args['syslog_port'] = self._syslog_port + api_args['syslog_port'] = syslog_port if syslog_ident: - api_args['syslog_ident'] = self._syslog_ident + api_args['syslog_ident'] = syslog_ident if syslog_facility: - if syslog_facility not in self.valid_syslog_facilities: - raise DynectInvalidArgumentError('syslog_facility', - syslog_facility, - self.valid_syslog_facilities) - api_args['syslog_facility'] = self._syslog_facility + self.syslog_facility = syslog_facility + api_args['syslog_facility'] = self.syslog_facility if region: - api_args['region'] = [region._json for region in self._region] + api_args['region'] = [region._json for region in region] if monitor: - api_args['monitor'] = self._monitor.to_json() + api_args['monitor'] = monitor.to_json() if performance_monitor: - mon_args = self._performance_monitor.to_json() - api_args['performance_monitor'] = mon_args + api_args['performance_monitor'] = performance_monitor.to_json() if contact_nickname: - api_args['contact_nickname'] = self._contact_nickname + api_args['contact_nickname'] = contact_nickname # API expects a CSV string, not a list if isinstance(self.notify_events, list): @@ -750,55 +318,51 @@ def _post(self, contact_nickname, performance_monitor, region, ttl=None, api_args) self._build(response['data']) - def _get(self): - """Build an object around an existing DynECT RTTM Service""" - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - self._build(response['data']) - - def _update(self, api_args): - """Perform a PUT api call using this objects data""" - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - def _build(self, data): """Build the neccesary substructures under this :class:`RTTM`""" - for key, val in data.items(): - if key == 'region': - self._region = APIList(DynectSession.get_session, 'region') - for region in val: - code = region.pop('region_code', None) - pool = region.pop('pool', None) - status = region.pop('status', None) - r = RTTMRegion(self._zone, self._fqdn, code, pool, **region) - r._status = status - self._region.append(r) - elif key == 'monitor': - if self._monitor is not None: - self._monitor.zone = self._zone - self._monitor.fqdn = self._fqdn - else: - proto = val.pop('protocol', None) - inter = val.pop('interval', None) - self._monitor = Monitor(proto, inter, **val) - elif key == 'performance_monitor': - if self._performance_monitor is not None: - self._performance_monitor.zone = self._zone - self._performance_monitor.fqdn = self._fqdn - else: - proto = val.pop('protocol', None) - inter = val.pop('interval', None) - self._performance_monitor = PerformanceMonitor(proto, inter, - **val) - elif key == 'notify_events': - self._notify_events = [item.strip() for item in val.split(',')] - elif key == 'active': - self._active = Active(val) + if 'region' in data: + self.region = APIList(DynectSession.get_session, 'region') + for region in data.pop('region'): + code = region.pop('region_code', None) + pool = region.pop('pool', None) + status = region.pop('status', None) + r = RTTMRegion(self.zone, self.fqdn, code, pool, **region) + r._status = status + self.region.append(r) + self.region.uri = self.uri + if 'monitor' in data: + if self.monitor is not None: + self.monitor.zone = self.zone + self.monitor.fqdn = self.fqdn + else: + monitor = data.pop('monitor') + proto = monitor.pop('protocol', None) + inter = monitor.pop('interval', None) + self.monitor = Monitor(proto, inter, **monitor) + if 'performance_monitor' in data: + if self.performance_monitor is not None: + self.performance_monitor.zone = self.zone + self.performance_monitor.fqdn = self.fqdn else: - setattr(self, '_' + key, val) - self._region.uri = self.uri + monitor = data.pop('performance_monitor') + proto = monitor.pop('protocol', None) + inter = monitor.pop('interval', None) + self.performance_monitor = PerformanceMonitor(proto, inter, + **monitor) + if 'notify_events' in data: + self._notify_events = [item.strip() for item in + data.pop('notify_events').split(',')] + if 'active' in data: + self._active = Active(data.pop('active')) + super(RTTM, self)._build(data) + + def _update(self, **api_args): + if 'monitor' in api_args: + api_args['monitor'] = api_args['monitor'].to_json() + if 'performance_monitor' in api_args: + pm = api_args['performance_monitor'] + api_args['performance_monitor'] = pm.to_json() + super(RTTM, self)._update(**api_args) def get_rrset_report(self, ts): """Generates a report of regional response sets for this RTTM service @@ -807,9 +371,7 @@ def get_rrset_report(self, ts): :param ts: UNIX timestamp identifying point in time for the log report :return: dictionary containing rrset report data """ - api_args = {'zone': self._zone, - 'fqdn': self._fqdn, - 'ts': ts} + api_args = {'zone': self.zone, 'fqdn': self.fqdn, 'ts': ts} response = DynectSession.get_session().execute('/RTTMRRSetReport/', 'POST', api_args) return response['data'] @@ -825,8 +387,7 @@ def get_log_report(self, start_ts, end_ts=None): :return: dictionary containing log report data """ end_ts = end_ts or datetime.now() - api_args = {'zone': self._zone, - 'fqdn': self._fqdn, + api_args = {'zone': self.zone, 'fqdn': self.fqdn, 'start_ts': unix_date(start_ts), 'end_ts': unix_date(end_ts)} response = DynectSession.get_session().execute('/RTTMLogReport/', @@ -835,20 +396,15 @@ def get_log_report(self, start_ts, end_ts=None): def activate(self): """Activate this RTTM Service""" - api_args = {'activate': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) + self._update(activate=True) def deactivate(self): """Deactivate this RTTM Service""" - api_args = {'deactivate': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) + self._update(deactivate=True) def recover(self, recoverip=None, address=None): - """Recovers the RTTM service or a specific node IP within the service""" + """Recovers the RTTM service or a specific node IP within the service + """ api_args = {'recover': True} if recoverip: api_args['recoverip'] = recoverip @@ -878,150 +434,9 @@ def active(self, value): elif value in activate and not self.active: self.activate() - @property - def auto_recover(self): - """Indicates whether or not the service should automatically come out of - failover when the IP addresses resume active status or if the service - should remain in failover until manually reset. Must be one of 'Y' or - 'N' - """ - return self._auto_recover - @auto_recover.setter - def auto_recover(self, value): - if value not in ('Y', 'N'): - raise DynectInvalidArgumentError('auto_recover', value, - ('Y', 'N')) - api_args = {'auto_recover': value} - self._update(api_args) - - @property - def ttl(self): - """Time To Live in seconds of records in the service. Must be less than - 1/2 of the Health Probe's monitoring interval. Must be one of 30, 60, - 150, 300, or 450. - """ - return self._ttl - @ttl.setter - def ttl(self, value): - if value not in self.valid_ttls: - raise DynectInvalidArgumentError('ttl', value, self.valid_ttls) - api_args = {'ttl': value} - self._update(api_args) - - @property - def notify_events(self): - """A list of events which trigger notifications. Valid values are: 'ip', - 'svc', and 'nosrv' - """ - return self._notify_events - @notify_events.setter - def notify_events(self, value): - for val in value: - if val not in self.valid_notify_events: - raise DynectInvalidArgumentError('notify_events', val, - self.valid_notify_events) - value = ', '.join(value) - api_args = {'notify_events': value} - self._update(api_args) - - @property - def syslog_server(self): - """The Hostname or IP address of a server to receive syslog - notifications on monitoring events - """ - return self._syslog_server - @syslog_server.setter - def syslog_server(self, value): - api_args = {'syslog_server': value} - self._update(api_args) - - @property - def syslog_port(self): - """The port where the remote syslog server listens for notifications""" - return self._syslog_port - @syslog_port.setter - def syslog_port(self, value): - api_args = {'syslog_port': value} - self._update(api_args) - - @property - def syslog_ident(self): - """The ident to use when sending syslog notifications""" - return self._syslog_ident - @syslog_ident.setter - def syslog_ident(self, value): - api_args = {'syslog_ident': value} - self._update(api_args) - - @property - def syslog_facility(self): - """The syslog facility to use when sending syslog notifications. Must - be one of kern, user, mail, daemon, auth, syslog, lpr, news, uucp, cron, - authpriv, ftp, ntp, security, console, local0, local1, local2, local3, - local4, local5, local6, or local7 - """ - return self._syslog_facility - @syslog_facility.setter - def syslog_facility(self, value): - if value not in self.valid_syslog_facilities: - raise DynectInvalidArgumentError('syslog_facility', value, - self.valid_syslog_facilities) - api_args = {'syslog_facility': value} - self._update(api_args) - - @property - def region(self): - """A list of :class:`RTTMRegion`'s""" - return self._region - @region.setter - def region(self, value): - if isinstance(value, list) and not isinstance(value, APIList): - self._region = APIList(DynectSession.get_session, 'region', None, - value) - elif isinstance(value, APIList): - self._region = value - self._region.uri = self.uri - - @property - def monitor(self): - """The :class:`Monitor` for this service""" - return self._monitor - @monitor.setter - def monitor(self, value): - if isinstance(value, Monitor): - self._monitor = value - api_args = {'monitor': self._monitor.to_json()} - self._update(api_args) - - @property - def performance_monitor(self): - """The Performance :class:`Monitor` for this service""" - return self._performance_monitor - @performance_monitor.setter - def performance_monitor(self, value): - if isinstance(value, Monitor): - self._performance_monitor = value - api_args = {'performance_monitor': - self._performance_monitor.to_json()} - self._update(api_args) - - @property - def contact_nickname(self): - """The name of contact to receive notifications from this service""" - return self._contact_nickname - @contact_nickname.setter - def contact_nickname(self, value): - api_args = {'contact_nickname': value} - self._update(api_args) - - def delete(self): - """Delete this RTTM Service""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) - def __str__(self): """str override""" - return force_unicode(': {}').format(self._fqdn) + return force_unicode(': {0}').format(self.fqdn) __repr__ = __unicode__ = __str__ def __bytes__(self): From eca7b234c85baafe45c82b9c679dd79304fda179 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 20 Nov 2014 21:22:34 -0500 Subject: [PATCH 15/59] Fixed a handful of bugs --- dyn/tm/zones.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dyn/tm/zones.py b/dyn/tm/zones.py index 5be11aa..b8288f7 100644 --- a/dyn/tm/zones.py +++ b/dyn/tm/zones.py @@ -105,7 +105,7 @@ def _post(self, contact=None, ttl=60, serial_style='increment', self._xfer(master_ip, timeout) else: # Assign serial style here to force pre-api validation - self.serial_style = serial_style + self._serial_style = serial_style api_args = {'zone': self.name, 'rname': contact, 'ttl': ttl, 'serial_style': serial_style} @@ -348,7 +348,7 @@ def get_all_records(self): record[r_key] = r_val record['create'] = False list_records.append(constructor(self.name, self.fqdn, - **record)) + api=False, **record)) records[key] = list_records return records @@ -561,6 +561,7 @@ def __init__(self, zone, *args, **kwargs): transfer requests to this zone's master """ self.uri = self.uri.format(zone_name=zone) + self._zone = zone super(SecondaryZone, self).__init__(*args, **kwargs) def _post(self, masters, contact_nickname=None, tsig_key_name=None): From bf9b103563aa2f4131d81318278157abb74ffef5 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 20 Nov 2014 21:23:01 -0500 Subject: [PATCH 16/59] Removed Junk class --- dyn/core.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/dyn/core.py b/dyn/core.py index 5bc4094..ea71f86 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -154,23 +154,6 @@ def __set__(self, instance, value): setattr(instance, self.private_name, value) -# class Junk(object): -# data = APIDescriptor('data') -# odds = ValidatedAttribute('odds', validator=[1, 3, 5, 7]) -# read_only = ImmutableAttribute('read_only') -# my_int = IntegerAttribute('my_int') -# my_str = StringAttribute('my_str') -# my_list = ListAttribute('my_list') -# -# def __init__(self): -# self._data = {'serial': 121312, 'ts': 12321111233} -# self._odds = 5 -# self._read_only = 'passw0rd' -# self._my_int = 0 -# self._my_str = 'lulz' -# self._my_list = [0, 1, 2] - - # noinspection PyUnusedLocal class APIObject(object): """Base API Object type responsible for handling shared functionality From 42f8c0edfe3eb0b966e6db31192f025b12d39e08 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 22 Nov 2014 17:49:12 -0500 Subject: [PATCH 17/59] Initial pass at porting dyn.tm.records to use the new APIObject and descriptor patterns, still need to complete docs --- dyn/tm/records.py | 1972 ++++----------------------------------------- 1 file changed, 150 insertions(+), 1822 deletions(-) diff --git a/dyn/tm/records.py b/dyn/tm/records.py index e282790..c363e87 100644 --- a/dyn/tm/records.py +++ b/dyn/tm/records.py @@ -4,8 +4,9 @@ These DNS_Records should really only need to be created via a zone instance but could also be created independently if passed valid zone, fqdn data """ -from .errors import DynectInvalidArgumentError from .session import DynectSession +from ..core import (APIObject, ImmutableAttribute, IntegerAttribute, + StringAttribute, ValidatedAttribute) from ..compat import force_unicode __author__ = 'jnappi' @@ -16,74 +17,67 @@ 'NSRecord', 'SOARecord', 'SPFRecord', 'SRVRecord', 'TXTRecord'] -class DNSRecord(object): +# noinspection PyMissingConstructor +class DNSRecord(APIObject): """Base record object contains functionality to be used across all other record type objects """ + session_type = DynectSession + zone = ImmutableAttribute('zone') + fqdn = ImmutableAttribute('fqdn') + record_id = ImmutableAttribute('record_id') + ttl = IntegerAttribute('ttl') + record_type = '' + + def __init__(self, zone, fqdn, record_id=None, *args, **kwargs): + self._zone, self._fqdn, self._record_id = zone, fqdn, record_id + self.uri = '/{0}/{1}/{2}/'.format(self.record_type, zone, fqdn) + if 'api' in kwargs: + del kwargs['api'] + self._build(kwargs) + elif record_id: + self._get(record_id) + else: + self._post(*args, **kwargs) + self.uri += '{0}/'.format(self.record_id) - def __init__(self, zone, fqdn, create=True): - super(DNSRecord, self).__init__() - self._zone = zone - self._fqdn = fqdn - self._ttl = None - self._record_type = None - self._record_id = None - self.create = create - self.api_args = {'rdata': {}} - - def _create_record(self, api_args): + def _post(self, *args, **api_args): """Make the API call to create the current record type :param api_args: arguments to be pased to the API call """ - if self.create: - if not self._fqdn.endswith('.'): - self._fqdn += '.' - if not self._record_type.endswith('Record'): - self._record_type += 'Record' - uri = '/{}/{}/{}/'.format(self._record_type, self._zone, - self._fqdn) - response = DynectSession.get_session().execute(uri, 'POST', - api_args) - self._build(response['data']) - - def _get_record(self, record_id): + response = DynectSession.get_session().execute(self.uri, 'POST', + api_args) + self._build(response['data']) + + def _get(self, record_id): """Get an existing record object from the DynECT System :param record_id: The id of the record you would like to get """ - if self.create: - if not self._fqdn.endswith('.'): - self._fqdn += '.' - if not self._record_type.endswith('Record'): - self._record_type += 'Record' - self._record_id = record_id - uri = '/{}/{}/{}/{}/'.format(self._record_type, self._zone, - self._fqdn, self._record_id) - response = DynectSession.get_session().execute(uri, 'GET', {}) - self._build(response['data']) - - def _update_record(self, api_args): - """Make the API call to update the current record type + uri = self.uri + '{}/'.format(record_id) + response = DynectSession.get_session().execute(uri, 'GET') + self._build(response['data']) - :param api_args: arguments to be pased to the API call + def _update(self, **api_args): + """Build our records rdata api_argument attribute, and ship that off to + the API """ - if not self._fqdn.endswith('.'): - self._fqdn += '.' - if not self._record_type.endswith('Record'): - self._record_type += 'Record' - uri = '/{}/{}/{}/{}/'.format(self._record_type, self._zone, self._fqdn, - self._record_id) - response = DynectSession.get_session().execute(uri, 'PUT', api_args) - self._build(response['data']) + rdata = DNSRecord.rdata(self) + for key, val in api_args.items(): + if key in rdata: + rdata[key] = val + api_args['rdata'] = rdata + super(DNSRecord, self)._update(**api_args) def _build(self, data): - for key, val in data.items(): - if key == 'rdata': - for r_key, r_val in val.items(): - setattr(self, '_' + r_key, r_val) - else: - setattr(self, '_' + key, val) + """Flatten the inner rdata dict for convenience and ship that data off + to be built + """ + if 'rdata' in data: + for r_key, r_val in data.pop('rdata').items(): + data[r_key] = r_val + super(DNSRecord, self)._build(data) def rdata(self): """Return a records rdata""" @@ -91,7 +85,8 @@ def rdata(self): for key, val in self.__dict__.items(): if key.startswith('_') and not hasattr(val, '__call__') \ and key != '_record_type' and key != '_record_id': - if 'ttl' not in key and 'zone' not in key and 'fqdn' not in key: + if 'ttl' not in key and 'zone' not in key \ + and 'fqdn' not in key: rdata[key[1:]] = val return rdata @@ -106,62 +101,12 @@ def geo_rdata(self): @property def rec_name(self): - return self._record_type.replace('Record', '').lower() - - def delete(self): - """Delete the current record""" - api_args = {} - if not self._fqdn.endswith('.'): - self._fqdn += '.' - if not self._record_type.endswith('Record'): - self._record_type += 'Record' - uri = '/{}/{}/{}/{}/'.format(self._record_type, self.zone, self.fqdn, - self._record_id) - DynectSession.get_session().execute(uri, 'DELETE', api_args) - - @property - def zone(self): - """Once the zone is set, it will be a read only property""" - return self._zone - - @zone.setter - def zone(self, value): - pass - - @property - def fqdn(self): - """Once the fqdn is set, it will be a read only property""" - return self._fqdn - - @fqdn.setter - def fqdn(self, value): - pass - - @property - def record_id(self): - """The unique ID of this record from the DynECT System""" - return self._record_id - - @record_id.setter - def record_id(self, value): - pass - - @property - def ttl(self): - """The TTL for this record""" - return self._ttl - - @ttl.setter - def ttl(self, value): - """Set the value of this record's ttl property""" - self._ttl = value - self.api_args['ttl'] = self._ttl - self._update_record(self.api_args) + """Convenience property to access the name of this record type""" + return self.record_type.replace('Record', '').lower() def __str__(self): """str override""" - return force_unicode('<{}>: {}').format(self._record_type, self._fqdn) - + return force_unicode('<{0}>: {1}').format(self.record_type, self.fqdn) __repr__ = __unicode__ = __str__ def __bytes__(self): @@ -172,37 +117,8 @@ def __bytes__(self): class ARecord(DNSRecord): """The IPv4 Address (A) Record forward maps a host name to an IPv4 address. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.ARecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param address: IPv4 address for the record - :param ttl: TTL for this record - """ - if 'create' in kwargs: - super(ARecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'ARecord' - else: - super(ARecord, self).__init__(zone, fqdn) - self._record_type = 'ARecord' - self._ttl = self._address = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - else: - self._post(*args, **kwargs) - - def _post(self, address, ttl=0): - """Create a new :class:`~dyn.tm.records.ARecord` on the DynECT System - """ - self._address = address - self._ttl = ttl - self.api_args = {'rdata': {'address': self._address}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'ARecord' + address = StringAttribute('address') def rdata(self): """Return this :class:`~dyn.tm.records.ARecord`'s rdata as a JSON blob @@ -211,65 +127,18 @@ def rdata(self): shell = {'a_rdata': guts} return shell - @property - def address(self): - """Return the value of this record's address property""" - return self._address - - @address.setter - def address(self, value): - """Set the value of this record's address property""" - self._address = value - if 'rdata' not in self.api_args: - self.api_args['rdata'] = {} - self.api_args['rdata']['address'] = self._address - self._update_record(self.api_args) - def __str__(self): """str override""" - return ': {}'.format(self._address) - - def __repr__(self): - """print override""" - return ': {}'.format(self._address) + return ': {0}'.format(self.address) + __repr__ = __str__ class AAAARecord(DNSRecord): """The IPv6 Address (AAAA) Record is used to forward map hosts to IPv6 addresses and is the current IETF recommendation for this purpose. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.AAAARecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param address: IPv6 address for the record - :param ttl: TTL for this record - """ - if 'create' in kwargs: - super(AAAARecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'AAAARecord' - else: - super(AAAARecord, self).__init__(zone, fqdn) - self._record_type = 'AAAARecord' - self._address = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - else: - self._post(*args, **kwargs) - - def _post(self, address, ttl=0): - """Create a new :class:`~dyn.tm.records.AAAARecord` on the DynECT - System - """ - self._address = address - self._ttl = ttl - self.api_args = {'rdata': {'address': self._address}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'AAAARecord' + address = StringAttribute('address') def rdata(self): """Return this :class:`~dyn.tm.records.AAAARecord`'s rdata as a JSON @@ -279,76 +148,21 @@ def rdata(self): shell = {'aaaa_rdata': guts} return shell - @property - def address(self): - """Return the value of this record's address property""" - return self._address - - @address.setter - def address(self, value): - """Set the value of this record's address property""" - self._address = value - self.api_args['rdata']['address'] = self._address - self._update_record(self.api_args) - def __str__(self): """str override""" - return ': {}'.format(self._address) - - def __repr__(self): - """print override""" - return ': {}'.format(self._address) + return ': {0}'.format(self.address) + __repr__ = __str__ class CERTRecord(DNSRecord): """The Certificate (CERT) Record may be used to store either public key certificates or Certificate Revocation Lists (CRL) in the zone file. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create a :class:`~dyn.tm.records.CERTRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param format: Numeric value for the certificate type - :param tag: Numeric value for the public key certificate - :param algorithm: Public key algorithm number used to generate the - certificate - :param certificate: The public key certificate - :param ttl: TTL for this record in seconds - """ - if 'create' in kwargs: - super(CERTRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'CERTRecord' - else: - super(CERTRecord, self).__init__(zone, fqdn) - self._record_type = 'CERTRecord' - self._format = self._tag = self._algorithm = None - self._certificate = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif len(args) + len(kwargs) == 1: - self._get_record(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, format, tag, algorithm, certificate, ttl=0): - """Create a new :class:`~dyn.tm.records.CERTRecord` on the DynECT - System - """ - self._format = format - self._tag = tag - self._algorithm = algorithm - self._certificate = certificate - self._ttl = ttl - self.api_args = {'rdata': {'format': self._format, - 'tag': self._tag, - 'algorithm': self._algorithm, - 'certificate': self._certificate}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'CERTRecord' + format = IntegerAttribute('format') + tag = IntegerAttribute('tag') + algorithm = StringAttribute('algorithm') + certificate = StringAttribute('certificate') def rdata(self): """Return this :class:`~dyn.tm.records.CERTRecord`'s rdata as a JSON @@ -358,87 +172,13 @@ def rdata(self): shell = {'cert_rdata': guts} return shell - @property - def format(self): - """Numeric value for the certificate type.""" - return self._format - - @format.setter - def format(self, value): - self._format = value - self.api_args['rdata']['format'] = self._format - self._update_record(self.api_args) - - @property - def tag(self): - """Numeric value for the public key certificate""" - return self._tag - - @tag.setter - def tag(self, value): - self._tag = value - self.api_args['rdata']['tag'] = self._tag - self._update_record(self.api_args) - - @property - def algorithm(self): - """Public key algorithm number used to generate the certificate""" - return self._algorithm - - @algorithm.setter - def algorithm(self, value): - self._algorithm = value - self.api_args['rdata']['algorithm'] = self._algorithm - self._update_record(self.api_args) - - @property - def certificate(self): - """The public key certificate""" - return self._certificate - - @certificate.setter - def certificate(self, value): - self._certificate = value - self.api_args['rdata']['certificate'] = self._certificate - self._update_record(self.api_args) - class CNAMERecord(DNSRecord): """The Canonical Name (CNAME) Records map an alias to the real or canonical name that may lie inside or outside the current zone. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.CNAMERecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param cname: Hostname - :param ttl: TTL for this record - """ - if 'create' in kwargs: - super(CNAMERecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'CNAMERecord' - else: - super(CNAMERecord, self).__init__(zone, fqdn) - self._record_type = 'CNAMERecord' - self._cname = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - else: - self._post(*args, **kwargs) - - def _post(self, cname, ttl=0): - """Create a new :class:`~dyn.tm.records.CNAMERecord` on the DynECT - System - """ - self._cname = cname - self._ttl = ttl - self.api_args = {'rdata': {'cname': self._cname}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'CNAMERecord' + cname = StringAttribute('cname') def rdata(self): """Return this :class:`~dyn.tm.records.CNAMERecord`'s rdata as a JSON @@ -448,25 +188,6 @@ def rdata(self): shell = {'cname_rdata': guts} return shell - @property - def cname(self): - """Hostname""" - return self._cname - - @cname.setter - def cname(self, value): - self._cname = value - self.api_args['rdata']['cname'] = self._cname - self._update_record(self.api_args) - - def __eq__(self, other): - """Equivalence override""" - if isinstance(other, CNAMERecord): - return self.cname == other.cname - elif isinstance(other, str): - return self.cname == other - return False - class DHCIDRecord(DNSRecord): """The :class:`~dyn.tm.records.DHCIDRecord` provides a means by which DHCP @@ -474,42 +195,8 @@ class DHCIDRecord(DNSRecord): so that multiple DHCP clients and servers may deterministically perform dynamic DNS updates to the same zone. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.DHCIDRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param digest: Base-64 encoded digest of DHCP data - :param ttl: TTL for this record - """ - if 'create' in kwargs: - super(DHCIDRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'DHCIDRecord' - else: - super(DHCIDRecord, self).__init__(zone, fqdn) - self._record_type = 'DHCIDRecord' - self._digest = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif len(args) + len(kwargs) == 1: - self._get_record(*args, **kwargs) - elif 'digest' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) == 2: - self._post(*args, **kwargs) - - def _post(self, digest, ttl=0): - """Create a new :class:`~dyn.tm.records.DHCIDRecord` on the DynECT - System - """ - self._digest = digest - self._ttl = ttl - self.api_args = {'rdata': {'digest': self._digest}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'DHCIDRecord' + digest = StringAttribute('digest') def rdata(self): """Return this :class:`~dyn.tm.records.DHCIDRecord`'s rdata as a JSON @@ -519,59 +206,16 @@ def rdata(self): shell = {'dhcid_rdata': guts} return shell - @property - def digest(self): - """Base-64 encoded digest of DHCP data""" - return self._digest - - @digest.setter - def digest(self, value): - self._digest = value - self.api_args['rdata']['digest'] = self._digest - self._update_record(self.api_args) - class DNAMERecord(DNSRecord): """The Delegation of Reverse Name (DNAME) Record is designed to assist the delegation of reverse mapping by reducing the size of the data that must be - entered. DNAME's are designed to be used in conjunction with a bit label but - do not strictly require one. However, do note that without a bit label a - DNAME is equivalent to a CNAME when used in a reverse-map zone file. + entered. DNAME's are designed to be used in conjunction with a bit label + but do not strictly require one. However, do note that without a bit label + a DNAME is equivalent to a CNAME when used in a reverse-map zone file. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.DNAMERecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param dname: Target Hostname - :param ttl: TTL for this record - """ - if 'create' in kwargs: - super(DNAMERecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'DNAMERecord' - else: - super(DNAMERecord, self).__init__(zone, fqdn) - self._record_type = 'DNAMERecord' - self._dname = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'dname' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) == 2: - self._post(*args, **kwargs) - - def _post(self, dname, ttl=0): - """Create a new :class:`~dyn.tm.records.DNAMERecord` on the DynECT - System - """ - self._dname = dname - self._ttl = ttl - self.api_args = {'rdata': {'dname': self._dname}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'DNAMERecord' + dname = StringAttribute('dname') def rdata(self): """Return this :class:`~dyn.tm.records.DNAMERecord`'s rdata as a JSON @@ -581,76 +225,17 @@ def rdata(self): shell = {'dname_rdata': guts} return shell - @property - def dname(self): - """Target hostname""" - return self._dname - - @dname.setter - def dname(self, value): - self._dname = value - self.api_args['rdata']['dname'] = self._dname - self._update_record(self.api_args) - class DNSKEYRecord(DNSRecord): """The DNSKEY Record describes the public key of a public key (asymmetric) cryptographic algorithm used with DNSSEC.nis. It is typically used to authenticate signed keys or zones. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create a :class:`~dyn.tm.records.DNSKEYRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param protocol: Numeric value for protocol - :param public_key: The public key for the DNSSEC signed zone - :param algorithm: Numeric value representing the public key encryption - algorithm which will sign the zone. Must be one of 1 (RSA-MD5), 2 - (Diffie-Hellman), 3 (DSA/SHA-1), 4 (Elliptic Curve), or - 5 (RSA-SHA-1) - :param flags: Numeric value confirming this is the zone's DNSKEY - :param ttl: TTL for this record. Use 0 for zone default - """ - if 'create' in kwargs: - super(DNSKEYRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'DNSKEYRecord' - else: - super(DNSKEYRecord, self).__init__(zone, fqdn) - self._record_type = 'DNSKEYRecord' - self._algorithm = self._flags = self._protocol = None - self._public_key = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif len(args) + len(kwargs) == 1: - self._get_record(*args, **kwargs) - elif 'protocol' in kwargs or 'public_key' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, protocol, public_key, algorithm=5, flags=256, - ttl=0): - """Create a new :class:`~dyn.tm.records.DNSKEYRecord` on the DynECT - System - """ - valid = range(1, 6) - if algorithm not in valid: - raise DynectInvalidArgumentError('algorthim', algorithm, valid) - self._algorithm = algorithm - self._flags = flags - self._protocol = protocol - self._public_key = public_key - self._ttl = ttl - self.api_args = {'rdata': {'algorithm': self._algorithm, - 'flags': self._flags, - 'protocol': self._protocol, - 'public_key': self._public_key}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'DNSKEYRecord' + protocol = IntegerAttribute('protocol') + public_key = StringAttribute('public_key') + algorithm = ValidatedAttribute('algorithm', validator=range(1, 6)) + flags = IntegerAttribute('flags') def rdata(self): """Return this :class:`~dyn.tm.records.DNSKEYRecord`'s rdata as a JSON @@ -660,109 +245,17 @@ def rdata(self): shell = {'dnskey_rdata': guts} return shell - @property - def algorithm(self): - """Public key encryption algorithm will sign the zone""" - return self._algorithm - - @algorithm.setter - def algorithm(self, value): - self._algorithm = value - self.api_args['rdata']['algorithm'] = self._algorithm - self._update_record(self.api_args) - - @property - def flags(self): - """Numeric value confirming this is the zone's DNSKEY""" - return self._flags - - @flags.setter - def flags(self, value): - self._flags = value - self.api_args['rdata']['flags'] = self._flags - self._update_record(self.api_args) - - @property - def protocol(self): - """Numeric value for protocol. Set to 3 for DNSSEC""" - return self._protocol - - @protocol.setter - def protocol(self, value): - self._protocol = value - self.api_args['rdata']['protocol'] = self._protocol - self._update_record(self.api_args) - - @property - def public_key(self): - """The public key for the DNSSEC signed zone""" - return self._public_key - - @public_key.setter - def public_key(self, value): - self._public_key = value - self.api_args['rdata']['public_key'] = self._public_key - self._update_record(self.api_args) - 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 zone. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create a :class:`~dyn.tm.records.DSRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param digest: The digest in hexadecimal form. 20-byte, - hexadecimal-encoded, one-way hash of the DNSKEY record surrounded - by parenthesis characters '(' & ')' - :param keytag: The digest mechanism to use to verify the digest - :param algorithm: Numeric value representing the public key encryption - algorithm which will sign the zone. Must be one of 1 (RSA-MD5), 2 - (Diffie-Hellman), 3 (DSA/SHA-1), 4 (Elliptic Curve), or - 5 (RSA-SHA-1) - :param digtype: the digest mechanism to use to verify the digest. Valid - values are SHA1, SHA256 - :param ttl: TTL for this record. Use 0 for zone default - """ - if 'create' in kwargs: - super(DSRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'DSRecord' - else: - super(DSRecord, self).__init__(zone, fqdn) - self._record_type = 'DSRecord' - self._algorithm = self._digest = self._digtype = self._keytag = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif len(args) + len(kwargs) == 1: - self._get_record(*args, **kwargs) - elif 'digest' in kwargs or 'keytag' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, digest, keytag, algorithm=5, digtype=1, ttl=0): - """Create a new :class:`~dyn.tm.records.DSRecord` on the DynECT System - """ - self._digest = digest - self._keytag = keytag - valid = range(1, 6) - if algorithm not in valid: - raise DynectInvalidArgumentError('algorthim', algorithm, valid) - self._algorithm = algorithm - self._digtype = digtype - self._ttl = ttl - self.api_args = {'rdata': {'algorithm': self._algorithm, - 'digest': self._digest, - 'digtype': self._digtype, - 'keytag': self._keytag}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'DSRecord' + digest = StringAttribute('digest') + keytag = IntegerAttribute('keytag') + algorithm = ValidatedAttribute('algorithm', validator=range(1, 6)) + digtype = ValidatedAttribute('digtype', validator=('SHA1', 'SHA256')) def rdata(self): """Return this :class:`~dyn.tm.records.DSRecord`'s rdata as a JSON blob @@ -771,52 +264,6 @@ def rdata(self): shell = {'ds_rdata': guts} return shell - @property - def algorithm(self): - """Identifies the encoding algorithm""" - return self._algorithm - - @algorithm.setter - def algorithm(self, value): - self._algorithm = value - self.api_args['rdata']['algorithm'] = self._algorithm - self._update_record(self.api_args) - - @property - def digest(self): - """The digest in hexadecimal form. 20-byte, hexadecimal-encoded, - one-way hash of the DNSKEY record surrounded by parenthesis characters - """ - return self._digest - - @digest.setter - def digest(self, value): - self._digest = value - self.api_args['rdata']['digest'] = self._digest - self._update_record(self.api_args) - - @property - def digtype(self): - """Identifies which digest mechanism to use to verify the digest""" - return self._digtype - - @digtype.setter - def digtype(self, value): - self._digtype = value - self.api_args['rdata']['digtype'] = self._digtype - self._update_record(self.api_args) - - @property - def keytag(self): - """Identifies which digest mechanism to use to verify the digest""" - return self._keytag - - @keytag.setter - def keytag(self, value): - self._keytag = value - self.api_args['rdata']['keytag'] = self._keytag - self._update_record(self.api_args) - class KEYRecord(DNSRecord): """"Public Key" (KEY) Records are used for the storage of public keys for @@ -826,58 +273,11 @@ class KEYRecord(DNSRecord): limited to use in DNS Security operations such as DDNS and zone transfer due to the difficulty of querying for specific uses. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create a :class:`~dyn.tm.records.KEYRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param algorithm: Numeric value representing the public key encryption - algorithm which will sign the zone. Must be one of 1 (RSA-MD5), 2 - (Diffie-Hellman), 3 (DSA/SHA-1), 4 (Elliptic Curve), or - 5 (RSA-SHA-1) - :param flags: See RFC 2535 for information on KEY record flags - :param protocol: Numeric identifier of the protocol for this KEY record - :param public_key: The public key for this record - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(KEYRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'KEYRecord' - else: - super(KEYRecord, self).__init__(zone, fqdn) - self._record_type = 'KEYRecord' - self._algorithm = self._flags = self._protocol = None - self._public_key = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif len(args) + len(kwargs) == 1: - self._get_record(*args, **kwargs) - elif 'algorithm' in kwargs or 'flags' in kwargs or 'protocol' in \ - kwargs or 'public_key' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, algorithm, flags, protocol, public_key, ttl=0): - """Create a new :class:`~dyn.tm.records.KEYRecord` on the DynECT System - """ - valid = range(1, 6) - if algorithm not in valid: - raise DynectInvalidArgumentError('algorthim', algorithm, valid) - self._algorithm = algorithm - self._flags = flags - self._protocol = protocol - self._public_key = public_key - self._ttl = ttl - self.api_args = {'rdata': {'algorithm': self._algorithm, - 'flags': self._flags, - 'protocol': self._protocol, - 'public_key': self._public_key}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'KEYRecord' + algorithm = ValidatedAttribute('algorithm', validator=range(1, 6)) + flags = IntegerAttribute('flags') + protocol = IntegerAttribute('protocol') + public_key = StringAttribute('public_key') def rdata(self): """Return this :class:`~dyn.tm.records.KEYRecord`'s rdata as a JSON @@ -887,99 +287,14 @@ def rdata(self): shell = {'key_rdata': guts} return shell - @property - def algorithm(self): - """Numeric identifier for algorithm used""" - return self._algorithm - - @algorithm.setter - def algorithm(self, value): - self._algorithm = value - self.api_args['rdata']['algorithm'] = self._algorithm - self._update_record(self.api_args) - - @property - def flags(self): - """See RFC 2535 for information about Key record flags""" - return self._flags - - @flags.setter - def flags(self, value): - self._flags = value - self.api_args['rdata']['flags'] = self._flags - self._update_record(self.api_args) - - @property - def protocol(self): - """Numeric identifier of the protocol for this KEY record""" - return self._protocol - - @protocol.setter - def protocol(self, value): - self._protocol = value - self.api_args['rdata']['protocol'] = self._protocol - self._update_record(self.api_args) - - @property - def public_key(self): - """The public key for this record""" - return self._public_key - - @public_key.setter - def public_key(self, value): - self._public_key = value - self.api_args['rdata']['public_key'] = self._public_key - self._update_record(self.api_args) - class KXRecord(DNSRecord): """The "Key Exchanger" (KX) Record type is provided with one or more alternative hosts. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create a :class:`~dyn.tm.records.KXRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param exchange: Hostname that will act as the Key Exchanger. The - hostname must have a :class:`~dyn.tm.records.CNAMERecord`, an - :class:`~dyn.tm.records.ARecord` and/or an - :class:`~dyn.tm.records.AAAARecord` associated with it - :param preference: Numeric value for priority usage. Lower value takes - precedence over higher value where two records of the same type - exist on the zone/node - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(KXRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'KXRecord' - else: - super(KXRecord, self).__init__(zone, fqdn) - self._record_type = 'KXRecord' - self._exchange = self._preference = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif len(args) + len(kwargs) == 1: - self._get_record(*args, **kwargs) - elif 'exchange' in kwargs or 'preference' in \ - kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, exchange, preference, ttl=0): - """Create a new :class:`~dyn.tm.records.KXRecord` on the DynECT System - """ - self._exchange = exchange - self._preference = preference - self._ttl = ttl - self.api_args = {'rdata': {'exchange': self._exchange, - 'preference': self._preference}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'KXRecord' + exchange = StringAttribute('exchange') + preference = IntegerAttribute('preference') def rdata(self): """Return this :class:`~dyn.tm.records.KXRecord`'s rdata as a JSON blob @@ -988,96 +303,19 @@ def rdata(self): shell = {'kx_rdata': guts} return shell - @property - def exchange(self): - """Hostname that will act as the Key Exchanger. The hostname must have - a CNAME record, an A Record and/or an AAAA record associated with it - """ - return self._exchange - - @exchange.setter - def exchange(self, value): - self._exchange = value - self.api_args['rdata']['exchange'] = self._exchange - self._update_record(self.api_args) - - @property - def preference(self): - """Numeric value for priority usage. Lower value takes precedence over - higher value where two records of the same type exist on the zone/node - """ - return self._preference - - @preference.setter - def preference(self, value): - self._preference = value - self.api_args['rdata']['preference'] = self._preference - self._update_record(self.api_args) - class LOCRecord(DNSRecord): """:class:`~dyn.tm.records.LOCRecord`'s allow for the definition of geographic positioning information associated with a host or service name. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create a :class:`~dyn.tm.records.LOCRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param altitude: Measured in meters above sea level - :param horiz_pre: - :param latitude: Measured in degrees, minutes, and seconds with N/S - indicator for North and South - :param longitude: Measured in degrees, minutes, and seconds with E/W - indicator for East and West - :param size: - :param version: - :param vert_pre: - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(LOCRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'LOCRecord' - else: - super(LOCRecord, self).__init__(zone, fqdn) - self._record_type = 'LOCRecord' - self._altitude = self._latitude = self._longitude = None - self._horiz_pre = self._size = self._vert_pre = None - # Version is required to be 0 - self._version = 0 - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'altitude' in kwargs or 'latitude' in \ - kwargs or 'longitude' in kwargs or 'horiz_pre' in \ - kwargs or 'size' in kwargs or 'vert_pre' in \ - kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, altitude, latitude, longitude, horiz_pre=10000, size=1, - vert_pre=10, ttl=0): - """Create a new :class:`~dyn.tm.records.LOCRecord` on the DynECT System - """ - self._altitude = altitude - self._latitude = latitude - self._longitude = longitude - self._horiz_pre = horiz_pre - self._size = size - self._vert_pre = vert_pre - self._ttl = ttl - self.api_args = {'rdata': {'altitude': self._altitude, - 'horiz_pre': self._horiz_pre, - 'latitude': self._latitude, - 'longitude': self._longitude, - 'size': self._size, - 'version': self._version, - 'vert_pre': self._vert_pre}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'LOCRecord' + altitude = IntegerAttribute('altitude') + horiz_pre = StringAttribute('horiz_pre') + latitude = IntegerAttribute('latitude') + longitude = IntegerAttribute('longitude') + size = IntegerAttribute('size') + version = ValidatedAttribute('version', validator=(0,)) + vert_pre = StringAttribute('vert_pre') def rdata(self): """Return this :class:`~dyn.tm.records.LOCRecord`'s rdata as a JSON @@ -1087,151 +325,17 @@ def rdata(self): shell = {'loc_rdata': guts} return shell - @property - def altitude(self): - """Measured in meters above sea level""" - return self._altitude - - @altitude.setter - def altitude(self, value): - self._altitude = value - self.api_args['rdata']['altitude'] = self._altitude - self._update_record(self.api_args) - - @property - def latitude(self): - """Measured in degrees, minutes, and seconds with N/S indicator for - North and South. Example: 45 24 15 N, where 45 = degrees, 24 = minutes, - 15 = seconds - """ - return self._latitude - - @latitude.setter - def latitude(self, value): - self._latitude = value - self.api_args['rdata']['latitude'] = self._latitude - self._update_record(self.api_args) - - @property - def longitude(self): - """Measured in degrees, minutes, and seconds with E/W indicator for - East and West. Example 89 23 18 W, where 89 = degrees, 23 = minutes, - 18 = seconds - """ - return self._longitude - - @longitude.setter - def longitude(self, value): - self._longitude = value - self.api_args['rdata']['longitude'] = self._longitude - self._update_record(self.api_args) - - @property - def horiz_pre(self): - """Defaults to 10,000 meters""" - return self._horiz_pre - - @horiz_pre.setter - def horiz_pre(self, value): - self._horiz_pre = value - self.api_args['rdata']['horiz_pre'] = self._horiz_pre - self._update_record(self.api_args) - - @property - def size(self): - """Defaults to 1 meter""" - return self._size - - @size.setter - def size(self, value): - self._size = value - self.api_args['rdata']['size'] = self._size - self._update_record(self.api_args) - - @property - def vert_pre(self): - return self._vert_pre - - @vert_pre.setter - def vert_pre(self, value): - self._vert_pre = value - self.api_args['rdata']['vert_pre'] = self._vert_pre - self._update_record(self.api_args) - - @property - def version(self): - """Number of the representation. Must be zero (0) - NOTE: Version has no setter, because it will never not be 0 - """ - return self._version - class IPSECKEYRecord(DNSRecord): """The IPSECKEY is used for storage of keys used specifically for IPSec oerations """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.IPSECKEYRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param precedence: Indicates priority among multiple IPSECKEYS. Lower - numbers are higher priority - :param gatetype: Gateway type. Must be one of 0, 1, 2, or 3 - :param algorithm: Public key's cryptographic algorithm and format. Must - be one of 0, 1, or 2 - :param gateway: Gateway used to create IPsec tunnel. Based on Gateway - type - :param public_key: Base64 encoding of the public key. Whitespace is - allowed - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(IPSECKEYRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'IPSECKEYRecord' - else: - super(IPSECKEYRecord, self).__init__(zone, fqdn) - self.valid_gatetypes = range(0, 4) - self.valid_algorithms = range(0, 3) - self._record_type = 'IPSECKEYRecord' - self._precedence = self._gatetype = self._algorithm = None - self._gateway = self._public_key = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'precedence' in kwargs or 'gatetype' in \ - kwargs or 'algorithm' in kwargs or 'gateway' in \ - kwargs or 'public_key' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, precedence, gatetype, algorithm, gateway, public_key, - ttl=0): - """Create a new :class:`~dyn.tm.records.IPSECKEYRecord` on the DynECT - System - """ - self._precedence = precedence - if gatetype not in self.valid_gatetypes: - raise DynectInvalidArgumentError('gatetype', gatetype, - self.valid_gatetypes) - self._gatetype = gatetype - if algorithm not in self.valid_algorithms: - raise DynectInvalidArgumentError('algorithm', algorithm, - self.valid_algorithms) - self._algorithm = algorithm - self._gateway = gateway - self._public_key = public_key - self._ttl = ttl - self.api_args = {'rdata': {'precedence': self._precedence, - 'gatetype': self._gatetype, - 'algorithm': self._algorithm, - 'gateway': self._gateway, - 'public_key': self._public_key}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'IPSECKEYRecord' + precedence = IntegerAttribute('precedence') + gatetype = ValidatedAttribute('gatetype', validator=range(0, 4)) + algorithm = ValidatedAttribute('algorithm', validator=range(0, 3)) + gateway = IntegerAttribute('gateway') + public_key = StringAttribute('public_key') def rdata(self): """Return this :class:`~dyn.tm.records.IPSECKEYRecord`'s rdata as a @@ -1241,108 +345,14 @@ def rdata(self): shell = {'ipseckey_rdata': guts} return shell - @property - def precedence(self): - """Indicates priority among multiple IPSECKEYS. Lower numbers are - higher priority - """ - return self._precedence - - @precedence.setter - def precedence(self, value): - self._precedence = value - self.api_args['rdata']['precedence'] = self._precedence - self._update_record(self.api_args) - - @property - def gatetype(self): - """Gateway type. Must be one of 0, 1, 2, or 3""" - return self._gatetype - - @gatetype.setter - def gatetype(self, value): - self._gatetype = value - self.api_args['rdata']['gatetype'] = self._gatetype - self._update_record(self.api_args) - - @property - def algorithm(self): - """Public key's cryptographic algorithm and format""" - return self._algorithm - - @algorithm.setter - def algorithm(self, value): - self._algorithm = value - self.api_args['rdata']['algorithm'] = self._algorithm - self._update_record(self.api_args) - - @property - def gateway(self): - """Gateway used to create IPsec tunnel. Based on Gateway type""" - return self._gateway - - @gateway.setter - def gateway(self, value): - self._gateway = value - self.api_args['rdata']['gateway'] = self._gateway - self._update_record(self.api_args) - - @property - def public_key(self): - """Base64 encoding of the public key. Whitespace is allowed""" - return self._public_key - - @public_key.setter - def public_key(self, value): - self._public_key = value - self.api_args['rdata']['public_key'] = self._public_key - self._update_record(self.api_args) - class MXRecord(DNSRecord): """The "Mail Exchanger" record type specifies the name and relative preference of mail servers for a Zone. Defined in RFC 1035 """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.MXRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param exchange: Hostname of the server responsible for accepting mail - messages in the zone - :param preference: Numeric value for priority usage. Lower value takes - precedence over higher value where two records of the same type - exist on the zone/node. - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(MXRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'MXRecord' - else: - super(MXRecord, self).__init__(zone, fqdn) - self._record_type = 'MXRecord' - self._exchange = self._preference = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'exchange' in kwargs or 'preference' in \ - kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) >= 1: - self._post(*args, **kwargs) - - def _post(self, exchange, preference=10, ttl=0): - """Create a new :class:`~dyn.tm.records.MXRecord` on the DynECT System - """ - self._exchange = exchange - self._preference = preference - self._ttl = ttl - self.api_args = {'rdata': {'exchange': self._exchange, - 'preference': self._preference}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'MXRecord' + exchange = StringAttribute('exchange') + preference = IntegerAttribute('preference') def rdata(self): """Return this :class:`~dyn.tm.records.MXRecord`'s rdata as a JSON blob @@ -1351,97 +361,19 @@ def rdata(self): shell = {'mx_rdata': guts} return shell - @property - def exchange(self): - """Hostname of the server responsible for accepting mail messages in - the zone - """ - return self._exchange - - @exchange.setter - def exchange(self, value): - self._exchange = value - self.api_args['rdata']['exchange'] = self._exchange - self._update_record(self.api_args) - - @property - def preference(self): - """Numeric value for priority usage. Lower value takes precedence over - higher value where two records of the same type exist on the zone/node - """ - return self._preference - - @preference.setter - def preference(self, value): - self._preference = value - self.api_args['rdata']['preference'] = self._preference - self._update_record(self.api_args) - class NAPTRRecord(DNSRecord): """Naming Authority Pointer Records are a part of the Dynamic Delegation - Discovery System (DDDS). The NAPTR is a generic record that defines a `rule` - that may be applied to private data owned by a client application.""" - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.NAPTRRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param order: Indicates the required priority for processing NAPTR - records. Lowest value is used first. - :param preference: Indicates priority where two or more NAPTR records - have identical order values. Lowest value is used first. - :param services: Always starts with "e2u+" (E.164 to URI). After the - e2u+ there is a string that defines the type and optionally the - subtype of the URI where this :class:`~dyn.tm.records.NAPTRRecord` - points. - :param regexp: The NAPTR record accepts regular expressions - :param replacement: The next domain name to find. Only applies if this - :class:`~dyn.tm.records.NAPTRRecord` is non-terminal. - :param flags: Should be the letter "U". This indicates that this NAPTR - record terminal - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(NAPTRRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'NAPTRRecord' - else: - super(NAPTRRecord, self).__init__(zone, fqdn) - self._record_type = 'NAPTRRecord' - self._order = self._preference = self._flags = self._services = None - self._regexp = self._replacement = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'order' in kwargs or 'preference' in kwargs or 'services' in \ - kwargs or 'regexp' in kwargs or 'replacement' in \ - kwargs or 'flags' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, order, preference, services, regexp, replacement, - flags='U', ttl=0): - """Create a new :class:`~dyn.tm.records.NAPTRRecord` on the DynECT - System - """ - self._order = order - self._preference = preference - self._flags = flags - self._services = services - self._regexp = regexp - self._replacement = replacement - self._ttl = ttl - self.api_args = {'rdata': {'order': self._order, - 'preference': self._preference, - 'flags': self._flags, - 'services': self._services, - 'regexp': self._regexp, - 'replacement': self._replacement}, - 'ttl': self._ttl} - self._create_record(self.api_args) + Discovery System (DDDS). The NAPTR is a generic record that defines a + `rule` that may be applied to private data owned by a client application. + """ + record_type = 'NAPTRRecord' + order = IntegerAttribute('order') + preference = IntegerAttribute('preference') + services = StringAttribute('services') + regexp = StringAttribute('regexp') + replacement = StringAttribute('replacement') + flags = StringAttribute('flags') def rdata(self): """Return this :class:`~dyn.tm.records.NAPTRRecord`'s rdata as a JSON @@ -1451,121 +383,13 @@ def rdata(self): shell = {'naptr_rdata': guts} return shell - @property - def order(self): - """Indicates the required priority for processing NAPTR records. Lowest - value is used first - """ - return self._order - - @order.setter - def order(self, value): - self._order = value - self.api_args['rdata']['order'] = self._order - self._update_record(self.api_args) - - @property - def preference(self): - """Indicates priority where two or more NAPTR records have identical - order values. Lowest value is used first. - """ - return self._preference - - @preference.setter - def preference(self, value): - self._preference = value - self.api_args['rdata']['preference'] = self._preference - self._update_record(self.api_args) - - @property - def flags(self): - """Should be the letter "U". This indicates that this NAPTR record - terminal (E.164 number that maps directly to a URI) - """ - return self._flags - - @flags.setter - def flags(self, value): - self._flags = value - self.api_args['rdata']['flags'] = self._flags - self._update_record(self.api_args) - - @property - def services(self): - """Always starts with "e2u+" (E.164 to URI). After the e2u+ there is a - string that defines the type and optionally the subtype of the URI - where this NAPTR record points - """ - return self._services - - @services.setter - def services(self, value): - self._services = value - self.api_args['rdata']['services'] = self._services - self._update_record(self.api_args) - - @property - def regexp(self): - """The NAPTR record accepts regular expressions""" - return self._regexp - - @regexp.setter - def regexp(self, value): - self._regexp = value - self.api_args['rdata']['regexp'] = self._regexp - self._update_record(self.api_args) - - @property - def replacement(self): - """The next domain name to find. Only applies if this NAPTR record is - non-terminal - """ - return self._replacement - - @replacement.setter - def replacement(self, value): - self._replacement = value - self.api_args['rdata']['replacement'] = self._replacement - self._update_record(self.api_args) - class PTRRecord(DNSRecord): """Pointer Records are used to reverse map an IPv4 or IPv6 IP address to a host name """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create a :class:`~dyn.tm.records.PTRRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param ptrdname: The hostname where the IP address should be directed - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(PTRRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'PTRRecord' - else: - super(PTRRecord, self).__init__(zone, fqdn) - self._record_type = 'PTRRecord' - self._ptrdname = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'ptrdname' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, ptrdname, ttl=0): - """Create a new :class:`~dyn.tm.records.PTRRecord` on the DynECT System - """ - self._ptrdname = ptrdname - self._ttl = ttl - self.api_args = {'rdata': {'ptrdname': self._ptrdname}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'PTRRecord' + ptrdname = StringAttribute('ptrdname') def rdata(self): """Return this :class:`~dyn.tm.records.PTRRecord`'s rdata as a JSON @@ -1575,64 +399,16 @@ def rdata(self): shell = {'ptr_rdata': guts} return shell - @property - def ptrdname(self): - """Hostname where the IP address should be directed""" - return self._ptrdname - - @ptrdname.setter - def ptrdname(self, value): - self._ptrdname = value - self.api_args['rdata']['ptrdname'] = self._ptrdname - self._update_record(self.api_args) - class PXRecord(DNSRecord): """The X.400 to RFC 822 E-mail RR allows mapping of ITU X.400 format e-mail addresses to RFC 822 format e-mail addresses using a MIXER-conformant gateway. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.PXRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param preference: Sets priority for processing records of the same - type. Lowest value is processed first. - :param map822: mail hostname - :param mapx400: The domain name derived from the X.400 part of MCGAM - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(PXRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'PXRecord' - else: - super(PXRecord, self).__init__(zone, fqdn) - self._record_type = 'PXRecord' - self._preference = self._map822 = self._mapx400 = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'preference' in kwargs or 'map822' in kwargs or 'mapx400' in \ - kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, preference, map822, mapx400, ttl=0): - """Create a new :class:`~dyn.tm.records.PXRecord` on the DynECT System - """ - self._preference = preference - self._map822 = map822 - self._mapx400 = mapx400 - self._ttl = ttl - self.api_args = {'rdata': {'preference': self._preference, - 'map822': self._map822, - 'mapx400': self._mapx400}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'PXRecord' + preference = StringAttribute('preference') + map822 = StringAttribute('map822') + mapx400 = StringAttribute('mapx400') def rdata(self): """Return this :class:`~dyn.tm.records.PXRRecord`'s rdata as a JSON @@ -1642,81 +418,14 @@ def rdata(self): shell = {'pxr_rdata': guts} return shell - @property - def preference(self): - """Sets priority for processing records of the same type. Lowest value - is processed first - """ - return self._preference - - @preference.setter - def preference(self, value): - self._preference = value - self.api_args['rdata']['preference'] = self._preference - self._update_record(self.api_args) - - @property - def map822(self): - """mail hostname""" - return self._map822 - - @map822.setter - def map822(self, value): - self._map822 = value - self.api_args['rdata']['map822'] = self._map822 - self._update_record(self.api_args) - - @property - def mapx400(self): - """Enter the domain name derived from the X.400 part of MCGAM""" - return self._mapx400 - - @mapx400.setter - def mapx400(self, value): - self._mapx400 = value - self.api_args['rdata']['mapx400'] = self._mapx400 - self._update_record(self.api_args) - class NSAPRecord(DNSRecord): """The Network Services Access Point record is the equivalent of an RR for ISO's Open Systems Interconnect (OSI) system in that it maps a host name to an endpoint address. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.PXRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param nsap: Hex-encoded NSAP identifier - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(NSAPRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'NSAPRecord' - else: - super(NSAPRecord, self).__init__(zone, fqdn) - self._record_type = 'NSAPRecord' - self._nsap = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'nsap' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, nsap, ttl=0): - """Create a new :class:`~dyn.tm.records.NSAPRecord` on the DynECT - System - """ - self._nsap = nsap - self._ttl = ttl - self.api_args = {'rdata': {'nsap': self._nsap}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'NSAPRecord' + nsap = StringAttribute('nsap') def rdata(self): """Return this :class:`~dyn.tm.records.NSAPRecord`'s rdata as a JSON @@ -1726,17 +435,6 @@ def rdata(self): shell = {'nsap_rdata': guts} return shell - @property - def nsap(self): - """Hex-encoded NSAP identifier""" - return self._nsap - - @nsap.setter - def nsap(self, value): - self._nsap = value - self.api_args['rdata']['nsap'] = self._nsap - self._update_record(self.api_args) - class RPRecord(DNSRecord): """The Respnosible Person record allows an email address and some optional @@ -1745,45 +443,9 @@ class RPRecord(DNSRecord): public servers but can provide very useful contact data during diagnosis and debugging network problems. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.RPRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param mbox: Email address of the Responsible Person. - :param txtdname: Hostname where a TXT record exists with more - information on the responsible person. - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(RPRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'RPRecord' - else: - super(RPRecord, self).__init__(zone, fqdn) - self._record_type = 'RPRecord' - self._mbox = self._txtdname = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'mbox' in kwargs or 'txtdname' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, mbox, txtdname, ttl=0): - """Create a new :class:`~dyn.tm.records.RPRecord` on the DynECT System - """ - if '@' in mbox: - mbox = mbox.replace('@', '.') - self._mbox = mbox - self._txtdname = txtdname - self._ttl = ttl - self.api_args = {'rdata': {'mbox': self._mbox, - 'txtdname': self._txtdname}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'RPRecord' + mbox = StringAttribute('mbox') + txtdname = StringAttribute('txtdname') def rdata(self): """Return this :class:`~dyn.tm.records.RPRecord`'s rdata as a JSON blob @@ -1792,76 +454,14 @@ def rdata(self): shell = {'rp_rdata': guts} return shell - @property - def mbox(self): - """Email address of the Responsible Person. Data format: Replace @ - symbol with a dot '.' in the address - """ - return self._mbox - - @mbox.setter - def mbox(self, value): - self._mbox = value - self.api_args['rdata']['mbox'] = self._mbox - self._update_record(self.api_args) - - @property - def txtdname(self): - """Hostname where a TXT record exists with more information on the - responsible person - """ - return self._txtdname - - @txtdname.setter - def txtdname(self, value): - self._txtdname = value - self.api_args['rdata']['txtdname'] = self._txtdname - self._update_record(self.api_args) - class NSRecord(DNSRecord): - """Nameserver Records are used to list all the nameservers that will respond - authoritatively for a domain. + """Nameserver Records are used to list all the nameservers that will + respond authoritatively for a domain. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.NSRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param nsdname: Hostname of the authoritative Nameserver for the zone - :param service_class: Hostname of the authoritative Nameserver for the - zone - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(NSRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'NSRecord' - else: - super(NSRecord, self).__init__(zone, fqdn) - self._record_type = 'NSRecord' - self._nsdname = None - self._service_class = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'nsdname' in kwargs or 'service_class' in \ - kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, nsdname, service_class='', ttl=0): - """Create a new :class:`~dyn.tm.records.NSRecord` on the DynECT System - """ - self._nsdname = nsdname - self._service_class = service_class - self._ttl = ttl - self.api_args = {'rdata': {'nsdname': self._nsdname}, - 'ttl': self._ttl, - 'service_class': self._service_class} - self._create_record(self.api_args) + record_type = 'NSRecord' + nsdname = StringAttribute('nsdname') + service_class = StringAttribute('service_class') def rdata(self): """Return this :class:`~dyn.tm.records.NSRecord`'s rdata as a JSON blob @@ -1870,29 +470,6 @@ def rdata(self): shell = {'ns_rdata': guts} return shell - @property - def nsdname(self): - """Hostname of the authoritative Nameserver for the zone""" - return self._nsdname - - @nsdname.setter - def nsdname(self, value): - self._nsdname = value - self.api_args['rdata']['nsdname'] = self._nsdname - self._update_record(self.api_args) - - @property - def service_class(self): - """Hostname of the authoritative Nameserver for the zone""" - return self._service_class - - @service_class.setter - def service_class(self, value): - self._service_class = value - api_args = {'rdata': {'nsdname': self._nsdname}, - 'service_class': self._service_class} - self._update_record(api_args) - class SOARecord(DNSRecord): """The Start of Authority Record describes the global properties for the @@ -1900,30 +477,10 @@ class SOARecord(DNSRecord): time. NOTE: Dynect users do not have the permissions required to create or delete SOA records on the Dynect System. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.SOARecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - """ - if 'create' in kwargs: - super(SOARecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'SOARecord' - else: - super(SOARecord, self).__init__(zone, fqdn) - self._record_type = 'SOARecord' - self._rname = self._serial_style = self._minimum = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif len(args) > 0: - self._get_record(*args) - else: - # Users can not POST or DELETE SOA Records - pass - self.api_args = {'rdata': {'rname': self._rname}} + record_type = 'SOARecord' + rname = StringAttribute('rname') + serial_style = StringAttribute('serial_style') + minimum = IntegerAttribute('minimum') def rdata(self): """Return this :class:`~dyn.tm.records.SOARecord`'s rdata as a JSON @@ -1933,55 +490,6 @@ def rdata(self): shell = {'soa_rdata': guts} return shell - @property - def rname(self): - """Domain name which specifies the mailbox of the person responsible - for this zone - """ - return self._rname - - @rname.setter - def rname(self, value): - self._rname = value - self.api_args['rdata']['rname'] = self._rname - self._update_record(self.api_args) - - @property - def serial_style(self): - """The style of the zone's serial""" - return self._serial_style - - @serial_style.setter - def serial_style(self, value): - self._serial_style = value - api_args = {'rdata': {'rname': self._rname}, - 'serial_style': self._serial_style} - self._update_record(api_args) - - @property - def minimum(self): - """The minimum TTL for this :class:`~dyn.tm.records.SOARecord`""" - return self._minimum - - @minimum.setter - def minimum(self, value): - self._minimum = value - api_args = {'rdata': {'rname': self._rname, 'minimum': self._minimum}} - self._update_record(api_args) - - @property - def ttl(self): - """The TTL for this record""" - return self._ttl - - @ttl.setter - def ttl(self, value): - """Set the value of this SOARecord's ttl property""" - self._ttl = value - api_args = {'rdata': {'rname': self._rname}, - 'ttl': self._ttl} - self._update_record(api_args) - def delete(self): """Users can not POST or DELETE SOA Records""" pass @@ -1992,39 +500,8 @@ class SPFRecord(DNSRecord): Transfer Agent (MTA) to verify that the originating IP of an email from a sender is authorized to send main for the sender's domain. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.SPFRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param txtdata: Free text containing SPF record information - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(SPFRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'SPFRecord' - else: - super(SPFRecord, self).__init__(zone, fqdn) - self._record_type = 'SPFRecord' - self._txtdata = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'txtdata' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, txtdata, ttl=0): - """Create a new :class:`~dyn.tm.records.SPFRecord` on the DynECT System - """ - self._txtdata = txtdata - self._ttl = ttl - self.api_args = {'rdata': {'txtdata': self._txtdata}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'SPFRecord' + txtdata = StringAttribute('txtdata') def rdata(self): """Return this :class:`~dyn.tm.records.SPFRecord`'s rdata as a JSON @@ -2034,171 +511,33 @@ def rdata(self): shell = {'spf_rdata': guts} return shell - @property - def txtdata(self): - """Free text box containing SPF record information""" - return self._txtdata - - @txtdata.setter - def txtdata(self, value): - self._txtdata = value - self.api_args['rdata']['txtdata'] = self._txtdata - self._update_record(self.api_args) - class SRVRecord(DNSRecord): """The Services Record type allow a service to be associated with a host name. A user or application that wishes to discover where a service is located can interrogate for the relevant SRV that describes the service. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create a :class:`~dyn.tm.records.SRVRecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param port: Indicates the port where the service is running - :param priority: Numeric value for priority usage. Lower value takes - precedence over higher value where two records of the same type - exist on the zone/node - :param target: The domain name of a host where the service is running - on the specified port - :param weight: Secondary prioritizing of records to serve. Records of - equal priority should be served based on their weight. Higher values - are served more often - :param ttl: TTL for the record. Set to 0 to use zone default - """ - if 'create' in kwargs: - super(SRVRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'SRVRecord' - else: - super(SRVRecord, self).__init__(zone, fqdn) - self._record_type = 'SRVRecord' - self._port = self._priority = self._target = self._weight = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'port' in kwargs or 'priority' in kwargs or 'target' in \ - kwargs or 'weight' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) > 1: - self._post(*args, **kwargs) - - def _post(self, port, priority, target, weight, ttl=0): - """Create a new :class:`~dyn.tm.records.SRVRecord` on the DynECT System - """ - self._port = port - self._priority = priority - self._target = target - self._weight = weight - self._ttl = ttl - self.api_args = {'rdata': {'port': self._port, - 'priority': self._priority, - 'target': self._target, - 'weight': self._weight}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'SRVRecord' + txtdata = StringAttribute('txtdata') + port = IntegerAttribute('port') + priority = IntegerAttribute('priority') + target = StringAttribute('target') + weight = IntegerAttribute('weight') def rdata(self): - """Return this :class:`~dyn.tm.records.SRVRecord`'s rdata as a JSON - blob - """ + """Return this :class:`~dyn.tm.records.SRVRecord`'s rdata as JSON""" guts = super(SRVRecord, self).rdata() shell = {'srv_rdata': guts} return shell - @property - def port(self): - """Indicates the port where the service is running""" - return self._port - - @port.setter - def port(self, value): - self._port = value - self.api_args['rdata']['port'] = self._port - self._update_record(self.api_args) - - @property - def priority(self): - """Numeric value for priority usage. Lower value takes precedence over - higher value where two records of the same type exist on the zone/node - """ - return self._priority - - @priority.setter - def priority(self, value): - self._priority = value - self.api_args['rdata']['priority'] = self._priority - self._update_record(self.api_args) - - @property - def target(self): - """The domain name of a host where the service is running on the - specified `port` - """ - return self._target - - @target.setter - def target(self, value): - self._target = value - self.api_args['rdata']['target'] = self._target - self._update_record(self.api_args) - - @property - def weight(self): - """Secondary prioritizing of records to serve. Records of equal - priority should be served based on their weight. Higher values are - served more often - """ - return self._weight - - @weight.setter - def weight(self, value): - self._weight = value - self.api_args['rdata']['weight'] = self._weight - self._update_record(self.api_args) - class TXTRecord(DNSRecord): """The Text record type provides the ability to associate arbitrary text with a name. For example, it can be used to provide a description of the host, service contacts, or any other required system information. """ - - def __init__(self, zone, fqdn, *args, **kwargs): - """Create a new TXTRecord object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param txtdata: Free form text for this - :class:`~dyn.tm.records.TXTRecord` - :param ttl: TTL for the record. Set to 0 to use zone default - """ - if 'create' in kwargs: - super(TXTRecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'TXTRecord' - else: - super(TXTRecord, self).__init__(zone, fqdn) - self._record_type = 'TXTRecord' - self._txtdata = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif 'txtdata' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) == 2: - self._post(*args, **kwargs) - - def _post(self, txtdata, ttl=0): - """Create a new :class:`~dyn.tm.records.TXTRecord` on the DynECT System - """ - self._ttl = ttl - self._txtdata = txtdata - self.api_args = {'rdata': {'txtdata': self._txtdata}, - 'ttl': self._ttl} - self._create_record(self.api_args) + record_type = 'TXTRecord' + txtdata = StringAttribute('txtdata') def rdata(self): """Return this :class:`~dyn.tm.records.TXTRecord`'s rdata as a JSON @@ -2207,14 +546,3 @@ def rdata(self): guts = super(TXTRecord, self).rdata() shell = {'txt_rdata': guts} return shell - - @property - def txtdata(self): - """Free form text""" - return self._txtdata - - @txtdata.setter - def txtdata(self, value): - self._txtdata = value - self.api_args['rdata']['txtdata'] = self._txtdata - self._update_record(self.api_args) From 1ad1cd5213dca0168ddac82dd9331ff19e5ffdf2 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 22 Nov 2014 17:50:44 -0500 Subject: [PATCH 18/59] Small fix for handling records --- dyn/tm/zones.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dyn/tm/zones.py b/dyn/tm/zones.py index b8288f7..3e6dcdf 100644 --- a/dyn/tm/zones.py +++ b/dyn/tm/zones.py @@ -346,7 +346,6 @@ def get_all_records(self): # Unpack rdata for r_key, r_val in record['rdata'].items(): record[r_key] = r_val - record['create'] = False list_records.append(constructor(self.name, self.fqdn, api=False, **record)) records[key] = list_records @@ -721,7 +720,6 @@ def get_all_records_by_type(self, record_type): for key, val in record['rdata'].items(): record[key] = val del record['rdata'] - record['create'] = False records.append(constructor(self.zone, self.fqdn, **record)) return records @@ -746,7 +744,6 @@ def get_any_records(self): # Unpack rdata for r_key, r_val in record['rdata'].items(): record[r_key] = r_val - record['create'] = False list_records.append( constructor(self.zone, self.fqdn, **record)) records[key] = list_records @@ -762,7 +759,6 @@ def delete(self): def __str__(self): """str override""" return force_unicode(': {0}').format(self.fqdn) - __repr__ = __unicode__ = __str__ def __bytes__(self): From 1a4d014e14af932a1d45cbed648c522c60f255fd Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 22 Nov 2014 17:56:17 -0500 Subject: [PATCH 19/59] Small PEP fixes --- dyn/tm/tools.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dyn/tm/tools.py b/dyn/tm/tools.py index 5099f14..a9f4265 100644 --- a/dyn/tm/tools.py +++ b/dyn/tm/tools.py @@ -17,14 +17,14 @@ def change_ip(zone, from_ip, to, v6=False, publish=False): you want updated :param to: Either a list of ip addresses or a single ip address that will overwrite from_ip - :param v6: Boolean flag to specify if we're replacing ipv4 or ipv6 addresses - (ie, whether we're updating an ARecord or AAAARecord) + :param v6: Boolean flag to specify if we're replacing ipv4 or ipv6 + addresses (ie, whether we're updating an ARecord or AAAARecord) :param publish: A boolean flag denoting whether or not to publish changes after making them. You can optionally leave this as *False* and process the returned changeset prior to publishing your changes. :returns: A list of tuples of the form (fqdn, old, new) where fqdn is - the fqdn of the record that was updated, old was the old ip address, and - new is the new ip address. + the fqdn of the record that was updated, old was the old ip address, + and new is the new ip address. """ records = zone.get_all_records() records = records['aaaa_records'] if v6 else records['a_records'] @@ -60,14 +60,14 @@ def map_ips(zone, mapping, v6=False, publish=False): :param zone: The :class:`~dyn.tm.zones.Zone` you wish to update ips for :param mapping: A *dict* of the form {'old_ip': 'new_ip'} - :param v6: Boolean flag to specify if we're replacing ipv4 or ipv6 addresses - (ie, whether we're updating an ARecord or AAAARecord) + :param v6: Boolean flag to specify if we're replacing ipv4 or ipv6 + addresses (ie, whether we're updating an ARecord or AAAARecord) :param publish: A boolean flag denoting whether or not to publish changes after making them. You can optionally leave this as *False* and process the returned changeset prior to publishing your changes. :returns: A list of tuples of the form (fqdn, old, new) where fqdn is - the fqdn of the record that was updated, old was the old ip address, and - new is the new ip address. + the fqdn of the record that was updated, old was the old ip address, + and new is the new ip address. """ records = zone.get_all_records() records = records['aaaa_records'] if v6 else records['a_records'] From c43beabb7487ab700072fae507f871ae5e70f930 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 22 Nov 2014 17:57:05 -0500 Subject: [PATCH 20/59] Small PEP8 fixes --- dyn/tm/utils.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dyn/tm/utils.py b/dyn/tm/utils.py index ed470f3..f6a8888 100644 --- a/dyn/tm/utils.py +++ b/dyn/tm/utils.py @@ -59,7 +59,8 @@ def insert(self, index, item): return response def pop(self, *args, **kwargs): - """Handle the removal via pop of an item in this list via an API Call""" + """Handle the removal via pop of an item in this list via an API Call + """ response = super(APIList, self).pop(*args, **kwargs) self._update(self.__build_args()) return response @@ -100,8 +101,8 @@ class Active(object): """Object for intercepting the active attribute of most services which return a non-pythonic 'Y' or 'N' as the active status. This class aims to allow for more pythonic interactions with these attributes by allowing the - active field to be represented as either it's boolean representation or it's - string 'Y' or 'N' representation. + active field to be represented as either it's boolean representation or + it's string 'Y' or 'N' representation. """ def __init__(self, inp): """Accept either a string 'Y' or 'N' or a bool as input From b496cf0412458ef591f9bf976130c0bdb277f62c Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 22 Nov 2014 18:54:12 -0500 Subject: [PATCH 21/59] Made imports explicit. Added documentation --- dyn/tm/zones.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/dyn/tm/zones.py b/dyn/tm/zones.py index 3e6dcdf..04fcd65 100644 --- a/dyn/tm/zones.py +++ b/dyn/tm/zones.py @@ -5,10 +5,15 @@ from datetime import datetime from .utils import unix_date -from .errors import * -from .records import * +from .errors import DynectCreateError, DynectGetError +from .records import (ARecord, AAAARecord, CERTRecord, CNAMERecord, + DHCIDRecord, DNAMERecord, DNSKEYRecord, DSRecord, + KEYRecord, KXRecord, LOCRecord, IPSECKEYRecord, MXRecord, + NAPTRRecord, PTRRecord, PXRecord, NSAPRecord, RPRecord, + NSRecord, SOARecord, SPFRecord, SRVRecord, TXTRecord) from .session import DynectSession -from .services import * +from .services import (ActiveFailover, DynamicDNS, DNSSEC, TrafficDirector, + GSLB, ReverseDNS, RTTM) from ..core import (APIObject, IntegerAttribute, StringAttribute, ListAttribute, ImmutableAttribute, ValidatedAttribute) from ..compat import force_unicode @@ -58,15 +63,36 @@ def get_all_secondary_zones(): # noinspection PyUnresolvedReferences class Zone(APIObject): """A class representing a DynECT Zone""" + #: Primary Zone URI uri = '/Zone/{zone_name}/' + session_type = DynectSession + + #: The name of this Zone zone = ImmutableAttribute('zone') + + #: Alias to zone name = ImmutableAttribute('zone') + + #: The fully qualified domain name of this Zone fqdn = ImmutableAttribute('fqdn') + + #: The style of this zone's serial. Valid values are increment, epoch, day, + #: and minute serial_style = ValidatedAttribute('serial_style', validator=('increment', 'epoch', 'day', 'minute')) + + #: The current serial of this zone, the format of the serial will be + #: dependent on the value of serial_style serial = IntegerAttribute('serial') + + #: Convenience property for this :class:`Zone`. If a :class:`Zone` is + #: frozen, the status will read as `'frozen'`, if the :class:`Zones` is not + #: frozen the status will read as `'active'`. Because the API does not + #: return information about whether or not a :class:`Zones` is frozen there + #: will be a few cases where this status will be `None` in order to avoid + #: guessing what the current status actually is. status = StringAttribute('status') def __init__(self, name, *args, **kwargs): @@ -540,12 +566,27 @@ def __bytes__(self): class SecondaryZone(APIObject): """A class representing DynECT Secondary zones""" + # Secondary Zone URI uri = '/Secondary/{zone_name}/' + session_type = DynectSession + + #: The name of this secondary zone zone = StringAttribute('zone') + + #: Alias to zone name = StringAttribute('zone') + + #: A list of IPv4 or IPv6 addresses of the master nameserver(s) for this + #: zone masters = ListAttribute('masters') + + #: Name of the :class:`~dyn.tm.accounts.Contact` that will receive + #: notifications for this :class:`~dyn.tm.zones.SecondaryZone` contact_nickname = StringAttribute('contact_nickname') + + #: Name of the TSIG key that will be used to sign transfer requests to this + #: zone's master tsig_key_name = StringAttribute('tsig_key_name') def __init__(self, zone, *args, **kwargs): From 8dbbe63b627644c2744f94572c4149d277c925a2 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 22 Nov 2014 18:54:42 -0500 Subject: [PATCH 22/59] Quick PEP8 fix --- dyn/tm/errors.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dyn/tm/errors.py b/dyn/tm/errors.py index fb8406d..eedaa68 100644 --- a/dyn/tm/errors.py +++ b/dyn/tm/errors.py @@ -4,9 +4,9 @@ completely unexpected happens TODO: add a DynectInvalidPermissionsError """ -__all__ = ['DynectAuthError', 'DynectInvalidArgumentError', 'DynectCreateError', - 'DynectUpdateError', 'DynectGetError', 'DynectDeleteError', - 'DynectQueryTimeout'] +__all__ = ['DynectAuthError', 'DynectInvalidArgumentError', + 'DynectCreateError', 'DynectUpdateError', 'DynectGetError', + 'DynectDeleteError', 'DynectQueryTimeout'] __author__ = 'jnappi' From 3ff303a54ff395a2a5c7fd56f6feff6cddb54a1a Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 22 Nov 2014 19:16:40 -0500 Subject: [PATCH 23/59] Documentation. Small PEP8 fixes --- dyn/tm/accounts.py | 195 ++++++++++++++++++++++++++++++--------------- 1 file changed, 132 insertions(+), 63 deletions(-) diff --git a/dyn/tm/accounts.py b/dyn/tm/accounts.py index ec4afee..0ca6080 100644 --- a/dyn/tm/accounts.py +++ b/dyn/tm/accounts.py @@ -4,13 +4,14 @@ """ from .errors import DynectInvalidArgumentError from .session import DynectSession -from ..core import APIObject, ImmutableAttribute, StringAttribute +from ..core import (APIObject, ImmutableAttribute, StringAttribute, + ListAttribute, ValidatedAttribute) from ..compat import force_unicode __author__ = 'jnappi' __all__ = ['get_updateusers', 'get_users', 'get_permissions_groups', 'get_contacts', 'get_notifiers', 'UpdateUser', 'User', - 'PermissionsGroup', 'UserZone', 'Notifier', 'Contact'] + 'PermissionsGroup', 'Notifier', 'Contact'] def get_updateusers(search=None): @@ -164,11 +165,24 @@ class UpdateUser(APIObject): a :class:`~dyn.tm.accounts.User` which are tied to a specific Dynamic DNS services. """ + #: UpdateUser URI uri = '/UpdateUser/' session_type = DynectSession + + #: This UpdateUser's user_name. An UpdateUser's user_name is a read-only + #: property which can not be updated after the UpdateUser has been created. user_name = ImmutableAttribute('user_name') + + #: The current password for this UpdateUser. This password may be updated. password = StringAttribute('password') + + #: This UpdateUser's nickname. An UpdateUser's nickname is a read-only + #: property which can not be updated after the UpdateUser has been created. nickname = StringAttribute('nickname') + + #: The current status of this UpdateUser will be one of either 'active' or + #: 'blocked'. Blocked UpdateUser's are unable to log into the DynECT + #: System, while active UpdateUser's are. status = ImmutableAttribute('status') def _post(self, nickname, password): @@ -231,28 +245,74 @@ def __bytes__(self): # noinspection PyAttributeOutsideInit,PyUnresolvedReferences class User(APIObject): """DynECT System User object""" - uri = '/UpdateUser/{user_name}/' + #: DynECT User URI + uri = '/User/{user_name}/' + session_type = DynectSession + + #: This User's user_name. This is a read-only property. user_name = ImmutableAttribute('user_name') + + #: The first name of this User first_name = StringAttribute('first_name') + + #: The last name of this User last_name = StringAttribute('last_name') + + #: The nickname asociated with this User nickname = StringAttribute('nickname') + + #: password = StringAttribute('password') + + #: The organization this User belongs to organization = StringAttribute('organization') + + #: The primary phone number for this User phone = StringAttribute('phone') + + #: The primary address of this User address = StringAttribute('address') + + #: The primary address of this User, line 2 address_2 = StringAttribute('address_2') + + #: The city this User is from city = StringAttribute('city') + + #: The country this User is from country = StringAttribute('country') + + #: The fax number associated with this User fax = StringAttribute('fax') + + #: The primary notification email for this User notify_email = StringAttribute('notify_email') + + #: The primary pager email for this User pager_email = StringAttribute('pager_email') + + #: This User's zip code post_code = StringAttribute('post_code') + + #: The name of the Group that this User belongs to group_name = StringAttribute('group_name') + + #: The permissions associated with this User permision = StringAttribute('permission') + + #: The zones that this User has access to zone = StringAttribute('zone') + + #: The zones that this User is explicitly forbidden from forbid = StringAttribute('forbid') + + #: A website associated with this User website = StringAttribute('website') + + #: The current status of this User. Note, although this is a read only + #: property, it may be indirectly set by using the :meth:`block` and + #: :meth:`unblock` methods status = ImmutableAttribute('status') def __init__(self, user_name, *args, **kwargs): @@ -477,16 +537,35 @@ def __bytes__(self): class PermissionsGroup(APIObject): """A DynECT System Permissions Group object""" + #: The DynECT Permissions Group URI uri = '/PermissionGroup/{group_name}/' session_type = DynectSession + + #: The name of this Permissions Group group_name = StringAttribute('group_name') + + #: A description of this Permissions Group description = StringAttribute('description') - group_type = StringAttribute('group_type') + + #: The type of this Permissions Group. Valid values are plain and default + group_type = ValidatedAttribute('group_type', + validator=('plain', 'default')) + + #: If 'Y', all current users will be added to the group. Cannot be used if + #: user_name is passed in all_users = StringAttribute('all_users') + + #: A list of permissions to apply to this Permissions Group permission = StringAttribute('permission') + + #: A list of users who belong to this Permissions Group user_name = StringAttribute('user_name') + + #: A list of Permissions Group's that belong to this Permissions Group subgroup = StringAttribute('subgroup') - zone = StringAttribute('zone') + + #: A list of zones where this Permissions Group's permissions apply + zone = ListAttribute('zone') def __init__(self, group_name, *args, **kwargs): """Create a new permissions Group @@ -562,8 +641,8 @@ def add_permission(self, permission): :param permission: the permission to add to this user """ - uri = '/PermissionGroupPermissionEntry/{}/{}/'.format(self._group_name, - permission) + uri = '/PermissionGroupPermissionEntry/{0}/{1}/'.format( + self._group_name, permission) DynectSession.get_session().execute(uri, 'POST') self._permission.append(permission) @@ -654,67 +733,24 @@ def __bytes__(self): return bytes(self.__str__()) -# noinspection PyMissingConstructor -class UserZone(APIObject): - """A DynECT system UserZoneEntry""" - user_name = ImmutableAttribute('user_name') - zone_name = ImmutableAttribute('zone_name') - recurse = StringAttribute('recurse') - - def __init__(self, user_name, zone_name, recurse='Y'): - self._user_name = user_name - self._zone_name = zone_name - self._recurse = recurse - api_args = {'recurse': self._recurse} - self.uri = '/UserZoneEntry/{0}/{1}/'.format(self._user_name, - self._zone_name) - respnose = DynectSession.get_session().execute(self.uri, 'POST', - api_args) - self._build(respnose['data']) - - def update_zones(self, zone=None): - """Replacement list zones where the user will now have permissions. - Pass an empty list or omit the argument to clear the user's zone - permissions - - :param zone: a list of zone names where the user will now have - permissions - """ - if zone is None: - zone = [] - api_args = {'zone': []} - for zone_data in zone: - api_args['zone'].append({'zone_name': zone_data}) - respnose = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(respnose['data']) - - def delete(self): - """Delete this :class:`~dyn.tm.accounts.UserZone` object from the - DynECT System - """ - api_args = {'recurse': self.recurse} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) - - def __str__(self): - """Custom str method""" - return force_unicode(': {0}').format(self.user_name) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) - - # noinspection PyUnresolvedReferences,PyMissingConstructor class Notifier(APIObject): """DynECT System Notifier""" + #: The DynECT Notifier URI uri = '/Notifier/' session_type = DynectSession + + #: The unique DynECT system id for this Notifier notifier_id = ImmutableAttribute('notifier_id') + + #: A unique label for this Notifier label = StringAttribute('label') - recipients = None - services = None + + #: A list of recipients attached to this Notifier + recipients = ListAttribute('recipients') + + #: A list of services that this Notifier is attached to + services = ListAttribute('services') def __init__(self, *args, **kwargs): """Create a new :class:`~dyn.tm.accounts.Notifier` object @@ -756,7 +792,7 @@ def _get(self, notifier_id): """Get an existing :class:`~dyn.tm.accounts.Notifier` object from the DynECT System """ - self.uri = '/Notifier/{}/'.format(self._notifier_id) + self.uri = '/Notifier/{0}/'.format(notifier_id) response = DynectSession.get_session().execute(self.uri, 'GET') self._build(response['data']) @@ -772,23 +808,56 @@ def __bytes__(self): class Contact(APIObject): """A DynECT System Contact""" + #: The DynECT Contact URI uri = '/Contact/{nickname}/' session_type = DynectSession + + #: The nickname for this Contact nickname = StringAttribute('nickname') + + #: The primary email address associated with this Contact email = StringAttribute('email') + + #: The first name of this Contact first_name = StringAttribute('first_name') + + #: The last name of this Contact last_name = StringAttribute('last_name') + + #: The organization that this Contact belongs to organization = StringAttribute('organization') + + #: The primary phone number for this Contact phone = StringAttribute('phone') + + #: The primary address associated with this Contact address = StringAttribute('address') + + #: The address associated with this Contact, line 2 address_2 = StringAttribute('address_2') + + #: The city that this Contact is from city = StringAttribute('city') + + #: The country that this Contact is from country = StringAttribute('country') + + #: The primary fax number for this Contact fax = StringAttribute('fax') + + #: The primary notification email address for this Contact notify_email = StringAttribute('notify_email') + + #: The primary pager email address for this Contact pager_email = StringAttribute('pager_email') + + #: The zip code for this Contact post_code = StringAttribute('post_code') + + #: The state that this Contact is from state = StringAttribute('state') + + #: A website associated with this Contact website = StringAttribute('website') def __init__(self, nickname, *args, **kwargs): @@ -866,7 +935,7 @@ def _build(self, data): def _update(self, **api_args): if 'nickname' in api_args: api_args['new_nickname'] = api_args.pop('nickname') - super(Contact, self)._update(api_args) + super(Contact, self)._update(**api_args) def __str__(self): """Custom str method""" From dbff71607fa85b25226ee6e5bb5f47497bb8be83 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 22 Nov 2014 19:25:46 -0500 Subject: [PATCH 24/59] Added some new attribute types, documentation, and API response debug logging --- dyn/core.py | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/dyn/core.py b/dyn/core.py index ea71f86..d436ef2 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -63,19 +63,28 @@ class Singleton(_Singleton('SingletonMeta', (object,), {})): class APIDescriptor(object): + """A Base descriptor type for attributes of objects being constructed by + API responses + """ + def __init__(self, name=''): super(APIDescriptor, self).__init__() self.name = name self.private_name = '_' + self.name def __get__(self, instance, cls): + """Return a handle on the private instance of this descriptor""" return getattr(instance, self.private_name, None) def __set__(self, instance, value): + """Update the value of the private attribute represented by this + descriptor and, if one is available, call the update method of the + instance whose attribute is being updated + """ setattr(instance, self.private_name, value) if hasattr(instance, '_update'): args = {self.name: value} - getattr(instance, '_update')(**args) + instance._update(**args) class ImmutableAttribute(APIDescriptor): @@ -105,6 +114,11 @@ class StringAttribute(TypedAttribute): ty = string_types +class BooleanAttribute(TypedAttribute): + """API Attribute that may only be a string type""" + ty = bool + + class ListAttribute(TypedAttribute): """API Attribute that may only be a list""" ty = list @@ -124,6 +138,7 @@ class ValidatedAttribute(APIDescriptor): """An API Attribute whose value can be forced to a specific subset of values """ + def __init__(self, name='', validator=None): """An API field that must be one of a specific set of values @@ -147,6 +162,10 @@ def __set__(self, instance, value): class ValidatedListAttribute(ValidatedAttribute, ListAttribute): + """An atribute type checked as a list, with validators for the individual + items the list contains + """ + def __set__(self, instance, value): l = getattr(instance, self.private_name, []) for item in l: @@ -319,12 +338,12 @@ def connect(self): self._token = None self._conn = None if self.ssl: - msg = 'Establishing SSL connection to {}:{}'.format(self.host, - self.port) + msg = 'Establishing SSL connection to {0}:{1}'.format(self.host, + self.port) self.logger.info(msg) self._conn = HTTPSConnection(self.host, self.port, timeout=300) else: - msg = 'Establishing unencrypted connection to {}:{}' + msg = 'Establishing unencrypted connection to {0}:{1}' msg = msg.format(self.host, self.port) self.logger.info(msg) self._conn = HTTPConnection(self.host, self.port, timeout=300) @@ -367,6 +386,7 @@ def _handle_response(self, response, uri, method, raw_args, final): time.sleep(8) return self.execute(uri, method, raw_args, final=True) else: + self.logger.debug('response: {0}'.format(ret_val)) return self._process_response(ret_val, method) def _validate_uri(self, uri): @@ -384,7 +404,7 @@ def _validate_uri(self, uri): def _validate_method(self, method): """Validate the provided HTTP method type""" if method.upper() not in self._valid_methods: - msg = '{} is not a valid HTTP method. Please use one of {}' + msg = '{0} is not a valid HTTP method. Please use one of {1}' msg = msg.format(method, ', '.join(self._valid_methods)) raise ValueError(msg) @@ -425,7 +445,7 @@ def execute(self, uri, method, args=None, final=False): # Prepare arguments to send to API raw_args, args, uri = self._prepare_arguments(args, method, uri) - msg = 'uri: {}, method: {}, args: {}' + msg = 'uri: {0}, method: {1}, args: {2}' self.logger.debug(msg.format(uri, method, clean_args(json.loads(args)))) @@ -476,7 +496,7 @@ def poll_response(self, response, body): while response.status == 307: time.sleep(1) uri = response.getheader('Location') - self.logger.info('Polling {}'.format(uri)) + self.logger.info('Polling {0}'.format(uri)) self.send_command(uri, 'GET', '') response = self._conn.getresponse() @@ -494,7 +514,7 @@ def send_command(self, uri, method, args): self._conn.putrequest(method, uri) # Build headers - user_agent = 'dyn-py v{}'.format(__version__) + user_agent = 'dyn-py v{0}'.format(__version__) headers = {'Content-Type': self.content_type, 'User-Agent': user_agent} for key, val in self.extra_headers.items(): headers[key] = val @@ -520,14 +540,14 @@ def wait_for_job_to_complete(self, job_id, timeout=120): :param timeout: how long (in seconds) we should wait for a valid response before giving up on this request """ - self.logger.debug('Polling for job_id: {}'.format(job_id)) + self.logger.debug('Polling for job_id: {0}'.format(job_id)) start = datetime.now() - uri = '/Job/{}/'.format(job_id) + uri = '/Job/{0}/'.format(job_id) api_args = {} # response = self.execute(uri, 'GET', api_args) response = {'status': 'incomplete'} now = datetime.now() - self.logger.warn('Waiting for job {}'.format(job_id)) + self.logger.warn('Waiting for job {0}'.format(job_id)) too_long = (now - start).seconds < timeout while response['status'] is 'incomplete' and too_long: time.sleep(10) @@ -552,7 +572,7 @@ def __setstate__(cls, state): def __str__(self): """str override""" - return force_unicode('<{}>').format(self.name) + return force_unicode('<{0}>').format(self.name) __repr__ = __unicode__ = __str__ From 45a08cc1bf4c2f37e04da8d0a66f43aae7517da5 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 3 Dec 2014 11:11:10 -0500 Subject: [PATCH 25/59] APIList bug fix --- dyn/tm/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dyn/tm/utils.py b/dyn/tm/utils.py index f6a8888..5713743 100644 --- a/dyn/tm/utils.py +++ b/dyn/tm/utils.py @@ -85,7 +85,8 @@ def __build_args(self): def _update(self, api_args): """Private update (PUT) method""" if self.session_func is not None and self.uri is not None: - response = self.session_func().execute(self.uri, 'PUT', api_args) + response = self.session_func.get_session().execute(self.uri, 'PUT', + api_args) data = response['data'][self.name] for new_data, item in zip(data, self): item._update(new_data) From 307f75c3b564cb7eca88da589e13864f9d499a03 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 3 Dec 2014 11:12:35 -0500 Subject: [PATCH 26/59] small DNSSEC fixes --- dyn/tm/services/dnssec.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dyn/tm/services/dnssec.py b/dyn/tm/services/dnssec.py index eda34c0..c0cad23 100644 --- a/dyn/tm/services/dnssec.py +++ b/dyn/tm/services/dnssec.py @@ -53,8 +53,7 @@ def __init__(self, key_type, algorithm, bits, start_ts=None, lifetime=None, for key, val in kwargs.items(): setattr(self, key, val) - @property - def _json(self): + def to_json(self): """The JSON representation of this :class:`DNSSECKey` object""" json_blob = {'type': self.key_type, 'algorithm': self.algorithm, @@ -121,7 +120,7 @@ def _post(self, keys, contact_nickname, notify_events=None): self._keys += keys self._contact_nickname = contact_nickname self._notify_events = notify_events - api_args = {'keys': [key._json for key in self._keys], + api_args = {'keys': [key.to_json() for key in self._keys], 'contact_nickname': self._contact_nickname} for key, val in self.__dict__.items(): if val is not None and not hasattr(val, '__call__') and \ @@ -191,10 +190,10 @@ def keys(self): @keys.setter def keys(self, value): if isinstance(value, list) and not isinstance(value, APIList): - self._keys = APIList(DynectSession.get_session, 'keys', None, value) + self._keys = APIList(DynectSession.get_session, 'keys', self.uri, + value) elif isinstance(value, APIList): self._keys = value - self._keys.uri = self.uri def activate(self): """Activate this :class:`DNSSEC` service""" From ebcf80c4169d72c8ecd2e81eb64bb002a3c13080 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 3 Dec 2014 11:13:08 -0500 Subject: [PATCH 27/59] AFO Notify events and active fixes --- dyn/tm/services/active_failover.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/dyn/tm/services/active_failover.py b/dyn/tm/services/active_failover.py index eecca60..ff4c1ed 100644 --- a/dyn/tm/services/active_failover.py +++ b/dyn/tm/services/active_failover.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from ._shared import BaseMonitor -from ..utils import Active +from ..utils import Active, APIList from ..session import DynectSession from ...core import (APIObject, ImmutableAttribute, StringAttribute, ClassAttribute, IntegerAttribute, ValidatedListAttribute) @@ -20,7 +20,6 @@ def uri(self): raise ValueError -# noinspection PyUnresolvedReferences class ActiveFailover(APIObject): """With Active Failover, we monitor your Primary IP. If a failover event is detected, our system auto switches (hot swaps) to your dedicated back-up @@ -73,6 +72,7 @@ def __init__(self, zone, fqdn, *args, **kwargs): :param ttl: Time To Live in seconds of records in the service. Must be less than 1/2 of the Health Probe's monitoring interval """ + self._zone, self._fqdn = zone, fqdn self.uri = self.uri.format(zone=zone, fqdn=fqdn) super(ActiveFailover, self).__init__(*args, **kwargs) @@ -105,9 +105,10 @@ def _update(self, **api_args): api_args['monitor'] = api_args['monitor'].to_json() if 'notify_events' in api_args: api_args['notify_events'] = ', '.join(api_args['notify_events']) - for key, val in self.api_args: - if key not in api_args: - api_args[key] = val + if 'activate' not in api_args and 'deactivate' not in api_args: + for key, val in self.api_args.items(): + if key not in api_args: + api_args[key] = val super(ActiveFailover, self)._update(**api_args) def _build(self, data): @@ -116,6 +117,11 @@ def _build(self, data): self._monitor = AFOMonitor(**data.pop('monitor')) if 'active' in data: self._active = Active(data.pop('active')) + if 'notify_events' in data: + events = data.pop('notify_events').split(',') + filtered = [event.strip() for event in events if event.strip()] + self._notify_events = APIList(DynectSession, 'notify_events', + self.uri, filtered) super(ActiveFailover, self)._build(data) @property From 49312bfe90cde7642219be94fd1c9a0a5edb3b96 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 3 Dec 2014 13:21:21 -0500 Subject: [PATCH 28/59] More flushed out GSLB implementation --- dyn/tm/services/gslb.py | 164 ++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 98 deletions(-) diff --git a/dyn/tm/services/gslb.py b/dyn/tm/services/gslb.py index 99b5172..c2679ba 100644 --- a/dyn/tm/services/gslb.py +++ b/dyn/tm/services/gslb.py @@ -2,9 +2,9 @@ from ._shared import BaseMonitor from ..utils import APIList from ..session import DynectSession -from ...core import (APIObject, ImmutableAttribute, StringAttribute, - ValidatedAttribute, IntegerAttribute, ClassAttribute, - ListAttribute) +from ...core import (APIObject, APIService, ImmutableAttribute, + StringAttribute, ValidatedAttribute, IntegerAttribute, + ClassAttribute, ListAttribute) from ...compat import force_unicode __author__ = 'jnappi' @@ -22,7 +22,6 @@ def uri(self): class GSLBRegionPoolEntry(APIObject): - """:class:`GSLBRegionPoolEntry`""" uri = '/GSLBRegionPoolEntry/{zone}/{fqdn}/{region}/{address}/' session_type = DynectSession zone = ImmutableAttribute('zone') @@ -59,13 +58,9 @@ def _post(self, label=None, weight=None, serve_mode=None): """Create a new :class:`GSLBRegionPoolEntry` on the DynECT System""" uri = '/GSLBRegionPoolEntry/{0}/{1}/{2}/'.format(self.zone, self.fqdn, self.region_code) - api_args = {'address': self.address} - if label: - api_args['label'] = label - if weight: - api_args['weight'] = weight - if serve_mode: - api_args['serve_mode'] = serve_mode + api_args = {'address': self.address, 'label': label, 'weight': weight, + 'serve_mode': serve_mode} + api_args = {api_args[k] for k in api_args if api_args[k] is not None} response = DynectSession.get_session().execute(uri, 'POST', api_args) self._build(response['data']) @@ -74,11 +69,6 @@ def _update(self, **api_args): api_args['new_address'] = api_args.pop('address') super(GSLBRegionPoolEntry, self)._update(**api_args) - def sync(self): - """Sync this :class:`GSLBRegionPoolEntry` object with the DynECT System - """ - self._get() - def to_json(self): """Convert this object into a json blob""" output = {'address': self.address} @@ -103,7 +93,6 @@ def __bytes__(self): class GSLBRegion(APIObject): - """docstring for GSLBRegion""" uri = '/GSLBRegion/{zone}/{fqdn}/{region}/' session_type = DynectSession zone = ImmutableAttribute('zone') @@ -137,21 +126,31 @@ def __init__(self, zone, fqdn, region_code, *args, **kwargs): self.uri = self.uri.format(zone=zone, fqdn=fqdn, region=region_code) super(GSLBRegion, self).__init__(*args, **kwargs) + def add_entry(self, address, label=None, weight=None, serve_mode=None): + """Create a new :class:`GSLBRegionPoolEntry` with the provided + parameters and add it to this :class:`GSLBRegion` + + :param address: The IP address or FQDN of this Node IP + :param label: Identifying descriptive information for this + :param weight: A number in the range of 1-14 controlling the order in + which this :class:`GSLBRegionPoolEntry` will be served + :param serve_mode: Sets the behavior of this particular record. Must be + one of 'always', 'obey', 'remove', 'no' + :return: The newly created :class:`GSLBRegionPoolEntry` + """ + new_entry = GSLBRegionPoolEntry(self.zone, self.fqdn, self.region_code, + address, label, weight, serve_mode) + self._pool.append(new_entry) + return new_entry + def _post(self, pool, serve_count=None, failover_mode=None, failover_data=None): """Create a new :class:`GSLBRegion` on the DynECT System""" - self._pool = pool - self._serve_count = serve_count - self._failover_mode = failover_mode - self._failover_data = failover_data uri = '/GSLBRegion/{0}/{1}/'.format(self.zone, self.fqdn) - api_args = {'pool': self._pool.to_json()} - if serve_count: - api_args['serve_count'] = self._serve_count - if failover_mode: - api_args['failover_mode'] = self._failover_mode - if failover_data: - api_args['failover_data'] = self._failover_data + api_args = {'pool': pool.to_json(), 'serve_count': serve_count, + 'failover_mode': failover_mode, + 'failover_data': failover_data} + api_args = {api_args[k] for k in api_args if api_args[k] is not None} response = DynectSession.get_session()(uri, 'POST', api_args) self._build(response['data']) @@ -172,21 +171,17 @@ def _update(self, **api_args): api_args['pool'] = api_args['pool'].to_json() super(GSLBRegion, self)._update(**api_args) - def sync(self): - """Sync this :class:`GSLBRegion` object with the DynECT System""" - self._get() - @property - def _json(self): + def to_json(self): """Convert this :class:`GSLBRegion` to a json blob""" output = {'region_code': self.region_code, 'pool': [pool.to_json() for pool in self._pool]} - if self._serve_count: - output['serve_count'] = self._serve_count - if self._failover_mode: - output['failover_mode'] = self._failover_mode - if self._failover_data: - output['failover_data'] = self._failover_data + if self.serve_count: + output['serve_count'] = self.serve_count + if self.failover_mode: + output['failover_mode'] = self.failover_mode + if self.failover_data: + output['failover_data'] = self.failover_data return output def __str__(self): @@ -199,7 +194,7 @@ def __bytes__(self): return bytes(self.__str__()) -class GSLB(APIObject): +class GSLB(APIService): """A Global Server Load Balancing (GSLB) service""" uri = '/GSLB/{zone}/{fqdn}/' session_type = DynectSession @@ -233,10 +228,10 @@ def __init__(self, zone, fqdn, *args, **kwargs): active status or if the service should remain in failover until manually reset. Must be 'Y' or 'N' :param ttl: Time To Live in seconds of records in the service. Must be - less than 1/2 of the Health Probe's monitoring interval. Must be one - of 30, 60, 150, 300, or 450 - :param notify_events: A comma separated list of the events which trigger - notifications. Must be one of 'ip', 'svc', or 'nosrv' + less than 1/2 of the Health Probe's monitoring interval. Must be + one of 30, 60, 150, 300, or 450 + :param notify_events: A comma separated list of the events which + trigger notifications. Must be one of 'ip', 'svc', or 'nosrv' :param syslog_server: The Hostname or IP address of a server to receive syslog notifications on monitoring events :param syslog_port: The port where the remote syslog server listens for @@ -265,23 +260,14 @@ def _post(self, contact_nickname, region, auto_recover=None, ttl=None, syslog_ident='dynect', syslog_facility='daemon', monitor=None): """Create a new :class:`GSLB` service object on the DynECT System""" api_args = {'contact_nickname': contact_nickname, - 'region': [r._json for r in region]} - if auto_recover: - api_args['auto_recover'] = auto_recover - if ttl: - api_args['ttl'] = ttl - if notify_events: - api_args['notify_events'] = notify_events - if syslog_server: - api_args['syslog_server'] = syslog_server - if syslog_port: - api_args['syslog_port'] = syslog_port - if syslog_ident: - api_args['syslog_ident'] = syslog_ident - if syslog_facility: - api_args['syslog_facility'] = syslog_facility - if monitor: - api_args['monitor'] = monitor.to_json() + 'region': [r.to_json() for r in region], + 'auto_recover': auto_recover, 'ttl': ttl, + 'notify_events': notify_events, + 'syslog_server': syslog_server, 'syslog_port': syslog_port, + 'syslog_ident': syslog_ident, + 'syslog_facility': syslog_facility, + 'monitor': monitor.to_json()} + api_args = {api_args[k] for k in api_args if api_args[k] is not None} response = DynectSession.get_session().execute(self.uri, 'POST', api_args) self._build(response['data']) @@ -291,8 +277,6 @@ def _build(self, data): returned by an API call :param data: the data from the JSON respnose - :param region: Boolean flag specifying whether to rebuild the region - objects or not """ if 'region' in data: self._region = APIList(DynectSession.get_session, 'region') @@ -327,52 +311,36 @@ def _update(self, **api_args): api_args['monitor'] = monitor.to_json() super(GSLB, self)._update(**api_args) - def sync(self): - """Sync this :class:`GSLB` object with the DynECT System""" - self._get() - - def activate(self): - """Activate this :class:`GSLB` service on the DynECT System""" - self._update(activate=True) - - def deactivate(self): - """Deactivate this :class:`GSLB` service on the DynECT System""" - self._update(deactivate=True) - def recover(self, address=None): """Recover the GSLB service on the designated zone node or a specific node IP within the service """ - api_args = {} if address: - api_args['recoverip'] = True - api_args['address'] = address + api_args = {'recoverip': True, 'address': address} else: - api_args['recover'] = True + api_args = {'recover': True} response = DynectSession.get_session().execute(self.uri, 'PUT', api_args) self._build(response['data']) - @property - def active(self): - """Indicates if the service is active. When setting directly, rather - than using activate/deactivate valid arguments are 'Y' or True to - activate, or 'N' or False to deactivate. Note: If your service is - already active and you try to activate it, nothing will happen. And - vice versa for deactivation. - - :returns: An :class:`Active` object representing the current state of - this :class:`GSLB` Service + def add_region(self, region_code, pool, serve_count=None, + failover_mode=None, failover_data=None): + """Add a new :class:`GSLBRegion` to this :class:`GSLB` service + + :param region_code: ISO region code of this :class:`GSLBRegion` + :param pool: The IP Pool list for this :class:`GSLBRegion` + :param serve_count: How many records will be returned in each DNS + response + :param failover_mode: How the :class:`GSLBRegion` should failover. Must + be one of 'ip', 'cname', 'region', 'global' + :param failover_data: Dependent upon failover_mode. Must be one of + 'ip', 'cname', 'region', 'global' + :return: The newly created :class:`GSLBRegion` object """ - return self._active - @active.setter - def active(self, value): - deactivate = ('N', False) - activate = ('Y', True) - if value in deactivate and self.active: - self.deactivate() - elif value in activate and not self.active: - self.activate() + new_region = GSLBRegion(self.zone, self.fqdn, region_code, pool, + serve_count, failover_mode, failover_data) + self._region.append(new_region) + return new_region def __str__(self): """str override""" From b467a3c7f9d3c81c5cf333c225ad4aa10d444ae0 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 3 Dec 2014 13:21:58 -0500 Subject: [PATCH 29/59] Added APIService type to simplify needing to track active status and activate/deactivate functionality --- dyn/core.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/dyn/core.py b/dyn/core.py index d436ef2..94a7986 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -230,14 +230,49 @@ def delete(self, *args, **kwargs): if self.session_type is not None: self.session_type.get_session().execute(self.uri, 'DELETE') - @property - def __json__(self): + def to_json(self): """Generic JSON reprsentation of this object""" dict_obj = cleared_class_dict(self.__dict__) return {x: dict_obj[x] for x in dict_obj if x.startswith('_') and not x.startswith('__')} +class APIService(APIObject): + """:class:`APIObject` subclass with some additional info provided for + services that have "activate" and "deactivate" functionality to simplify + the ability to activate and deactivate the service + """ + + @property + def active(self): + """Indicates if the service is active. When setting directly, rather + than using activate/deactivate valid arguments are 'Y' or True to + activate, or 'N' or False to deactivate. Note: If your service is + already active and you try to activate it, nothing will happen. And + vice versa for deactivation. + + :returns: An :class:`Active` object representing the current state of + this :class:`GSLB` Service + """ + return self.active + @active.setter + def active(self, value): + deactivate = ('N', False) + activate = ('Y', True) + if value in deactivate and self.active: + self.deactivate() + elif value in activate and not self.active: + self.activate() + + def activate(self): + """Ensure that this service is currently activated""" + self._update(activate=True) + + def deactivate(self): + """Ensure that this service is currently deactivated""" + self._update(deactivate=True) + + class _History(list): """A *list* subclass specifically targeted at being able to store the history of calls made via a SessionEngine From 80fbc2180a1d12a5e8c1a5d5458286d559e2f622 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 3 Dec 2014 13:30:47 -0500 Subject: [PATCH 30/59] Completed AFO --- dyn/tm/services/active_failover.py | 65 +++++++++++++++--------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/dyn/tm/services/active_failover.py b/dyn/tm/services/active_failover.py index ff4c1ed..eeb3653 100644 --- a/dyn/tm/services/active_failover.py +++ b/dyn/tm/services/active_failover.py @@ -2,7 +2,7 @@ from ._shared import BaseMonitor from ..utils import Active, APIList from ..session import DynectSession -from ...core import (APIObject, ImmutableAttribute, StringAttribute, +from ...core import (APIService, ImmutableAttribute, StringAttribute, ClassAttribute, IntegerAttribute, ValidatedListAttribute) from ...compat import force_unicode @@ -20,7 +20,7 @@ def uri(self): raise ValueError -class ActiveFailover(APIObject): +class ActiveFailover(APIService): """With Active Failover, we monitor your Primary IP. If a failover event is detected, our system auto switches (hot swaps) to your dedicated back-up IP @@ -28,20 +28,51 @@ class ActiveFailover(APIObject): uri = '/Failover/{zone}/{fqdn}/' session_type = DynectSession + #: The zone that this :class:`ActiveFailover` service is attached to zone = ImmutableAttribute('zone') + + #: The fqdn of this :class:`ActiveFailover` service is attached to fqdn = ImmutableAttribute('fqdn') + + #: IPv4 Address or FQDN being monitored by this :class:`ActiveFailover` address = StringAttribute('address') + + #: The target failover resource type. failover_mode = StringAttribute('failover_mode') + + #: The IPv4 Address or CNAME data for the failover target failover_data = StringAttribute('failover_data') + + #: The :class:`AFOMonitor` for this :class:`ActiveFailover` service monitor = ClassAttribute('monitor', AFOMonitor) + + #: Name of contact to receive notifications for this service contact_nickname = StringAttribute('contact_nickname') + + #: Indicates whether this service should restore its original state when + #: the source IPs resume online status auto_recover = StringAttribute('auto_recover') + + #: A comma separated list of what events trigger notifications. Must be one + #: of 'ip', 'svc', or 'nosrv' notify_events = ValidatedListAttribute('notify_events', validator=('ip', 'svc', 'nosrv')) + + #: The Hostname or IP address of a server to receive syslog notifications + #: on monitoring events syslog_server = StringAttribute('syslog_server') + + #: The port where the remote syslog server listens syslog_port = IntegerAttribute('syslog_port') + + #: The ident to use when sending syslog notifications syslog_ident = StringAttribute('syslog_ident') + + #: The syslog facility to use when sending syslog notifications syslog_facility = StringAttribute('syslog_facility') + + #: TTL in seconds of records in the service. Must be less than 1/2 of the + #: :class:`AFOMonitor`'s monitoring interval ttl = IntegerAttribute('ttl') def __init__(self, zone, fqdn, *args, **kwargs): @@ -134,36 +165,6 @@ def api_args(self): 'monitor': self.monitor.to_json(), 'contact_nickname': self.contact_nickname} - @property - def active(self): - """Return whether or not this :class:`ActiveFailover` service is - active. When setting directly, rather than using activate/deactivate - valid arguments are 'Y' or True to activate, or 'N' or False to - deactivate. Note: If your service is already active and you try to - activate it, nothing will happen. And vice versa for deactivation. - - :returns: An :class:`Active` object representing the current state of - this :class:`ActiveFailover` Service - """ - self._get() - return self._active - @active.setter - def active(self, value): - deactivate = ('N', False) - activate = ('Y', True) - if value in deactivate and self.active: - self.deactivate() - elif value in activate and not self.active: - self.activate() - - def activate(self): - """Activate this :class:`ActiveFailover` service""" - self._update(activate=True) - - def deactivate(self): - """Deactivate this :class:`ActiveFailover` service""" - self._update(deactivate=True) - def __str__(self): """str override""" return force_unicode(': {0}').format(self.fqdn) From 8293052ef64da3cf68bb15b5496b0c2add4a33f9 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Wed, 3 Dec 2014 13:36:58 -0500 Subject: [PATCH 31/59] Completed DDNS work --- dyn/tm/services/ddns.py | 50 +++++++++++++---------------------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/dyn/tm/services/ddns.py b/dyn/tm/services/ddns.py index 9a66f17..508c0b3 100644 --- a/dyn/tm/services/ddns.py +++ b/dyn/tm/services/ddns.py @@ -4,23 +4,35 @@ from ..utils import Active from ..session import DynectSession from ..accounts import User -from ...core import APIObject, ImmutableAttribute, StringAttribute +from ...core import APIService, ImmutableAttribute, StringAttribute from ...compat import force_unicode __author__ = 'jnappi' __all__ = ['DynamicDNS'] -class DynamicDNS(APIObject): +class DynamicDNS(APIService): """DynamicDNS is a service which aliases a dynamic IP Address to a static hostname """ uri = '/DDNS/{zone}/{fqdn}/{rr_type}/' session_type = DynectSession + + #: The zone to attach this :class:`DynamicDNS` Service to zone = ImmutableAttribute('zone') + + #: The FQDN of the node where this service will be attached fqdn = ImmutableAttribute('fqdn') + + #: Either A, for IPv4, or AAAA, for IPv6 record_type = ImmutableAttribute('record_type') + + #: IPv4 (if `record_type` is A) or IPv6 (if `record_type` is AAAA) address + #: for the service address = StringAttribute('address') + + #: Name of the user to create, or the name of an existing update user to + #: allow access to this service user = ImmutableAttribute('user') def __init__(self, zone, fqdn, record_type, *args, **kwargs): @@ -57,38 +69,8 @@ def _post(self, address, user=None): if user: api_args['user'] = user api_args['full_setup'] = True - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) - self._build(response['data']) - - @property - def active(self): - """Returns whether or not this :class:`DynamicDNS` Service is currently - active. When setting directly, rather than using activate/deactivate - valid arguments are 'Y' or True to activate, or 'N' or False to - deactivate. Note: If your service is already active and you try to - activate it, nothing will happen. And vice versa for deactivation. - - :returns: An :class:`Active` object representing the current state of - this :class:`DynamicDNS` Service - """ - return self._active - @active.setter - def active(self, value): - deactivate = ('N', False) - activate = ('Y', True) - if value in deactivate and self.active: - self.deactivate() - elif value in activate and not self.active: - self.activate() - - def activate(self): - """Activate this Dynamic DNS service""" - self._update(activate=True) - - def deactivate(self): - """Deactivate this Dynamic DNS service""" - self._update(deactivate=True) + resp = DynectSession.get_session().execute(self.uri, 'POST', api_args) + self._build(resp['data']) def reset(self): """Resets the abuse count on this Dynamic DNS service""" From 12953eec5cbaccd6b097e2e2e107faf89aba7342 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 4 Dec 2014 10:28:41 -0500 Subject: [PATCH 32/59] __str__, __repr__, __unicode__, __bytes__ simplification --- dyn/core.py | 12 +++ dyn/tm/accounts.py | 34 +----- dyn/tm/records.py | 6 -- dyn/tm/services/_shared.py | 6 -- dyn/tm/services/active_failover.py | 6 -- dyn/tm/services/ddns.py | 6 -- dyn/tm/services/dnssec.py | 12 --- dyn/tm/services/gslb.py | 18 ---- dyn/tm/services/reversedns.py | 6 -- dyn/tm/services/rttm.py | 160 +++++++++++++---------------- dyn/tm/zones.py | 20 ---- 11 files changed, 83 insertions(+), 203 deletions(-) diff --git a/dyn/core.py b/dyn/core.py index 94a7986..6be7ff7 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -236,6 +236,18 @@ def to_json(self): return {x: dict_obj[x] for x in dict_obj if x.startswith('_') and not x.startswith('__')} + def __str__(self): + return force_unicode('') + + def __repr__(self): + return self.__str__() + + def __unicode__(self): + return self.__str__() + + def __bytes__(self): + return bytes(self.__str__()) + class APIService(APIObject): """:class:`APIObject` subclass with some additional info provided for diff --git a/dyn/tm/accounts.py b/dyn/tm/accounts.py index 0ca6080..eec0381 100644 --- a/dyn/tm/accounts.py +++ b/dyn/tm/accounts.py @@ -233,13 +233,7 @@ def delete(self): DynectSession.get_session().execute(self.uri, 'DELETE') def __str__(self): - """Custom str method""" - return force_unicode(': {}').format(self.user_name) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) + return force_unicode(': {0}').format(self.user_name) # noinspection PyAttributeOutsideInit,PyUnresolvedReferences @@ -526,13 +520,7 @@ def delete_forbid_rule(self, permission, zone=None): DynectSession.get_session().execute(uri, 'DELETE', api_args) def __str__(self): - """Custom str method""" - return force_unicode(': {}').format(self.user_name) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) + return force_unicode(': {0}').format(self.user_name) class PermissionsGroup(APIObject): @@ -724,13 +712,7 @@ def delete_subgroup(self, name): self._subgroup.remove(name) def __str__(self): - """Custom str method""" return force_unicode(': {0}').format(self.group_name) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) # noinspection PyUnresolvedReferences,PyMissingConstructor @@ -797,13 +779,7 @@ def _get(self, notifier_id): self._build(response['data']) def __str__(self): - """Custom str method""" return force_unicode(': {0}').format(self.label) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) class Contact(APIObject): @@ -938,10 +914,4 @@ def _update(self, **api_args): super(Contact, self)._update(**api_args) def __str__(self): - """Custom str method""" return force_unicode(': {0}').format(self.nickname) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) diff --git a/dyn/tm/records.py b/dyn/tm/records.py index c363e87..049d03d 100644 --- a/dyn/tm/records.py +++ b/dyn/tm/records.py @@ -105,13 +105,7 @@ def rec_name(self): return self.record_type.replace('Record', '').lower() def __str__(self): - """str override""" return force_unicode('<{0}>: {1}').format(self.record_type, self.fqdn) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) class ARecord(DNSRecord): diff --git a/dyn/tm/services/_shared.py b/dyn/tm/services/_shared.py index 145ab35..7abc328 100644 --- a/dyn/tm/services/_shared.py +++ b/dyn/tm/services/_shared.py @@ -96,11 +96,5 @@ def status(self): return respnose['data']['status'] def __str__(self): - """str override""" return force_unicode('<{0}>: {1}').format(self.__class__.__name__, self.protocol) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) diff --git a/dyn/tm/services/active_failover.py b/dyn/tm/services/active_failover.py index eeb3653..de9dc36 100644 --- a/dyn/tm/services/active_failover.py +++ b/dyn/tm/services/active_failover.py @@ -166,10 +166,4 @@ def api_args(self): 'contact_nickname': self.contact_nickname} def __str__(self): - """str override""" return force_unicode(': {0}').format(self.fqdn) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) diff --git a/dyn/tm/services/ddns.py b/dyn/tm/services/ddns.py index 508c0b3..d0b00a8 100644 --- a/dyn/tm/services/ddns.py +++ b/dyn/tm/services/ddns.py @@ -77,10 +77,4 @@ def reset(self): self._update(reset=True) def __str__(self): - """str override""" return force_unicode(': {0}').format(self.fqdn) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) diff --git a/dyn/tm/services/dnssec.py b/dyn/tm/services/dnssec.py index c0cad23..d80e6a1 100644 --- a/dyn/tm/services/dnssec.py +++ b/dyn/tm/services/dnssec.py @@ -80,13 +80,7 @@ def _update(self, data): setattr(self, key, val) def __str__(self): - """str override""" return force_unicode(': {0}').format(self.algorithm) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) class DNSSEC(APIObject): @@ -225,10 +219,4 @@ def timeline_report(self, start_ts=None, end_ts=None): return response['data'] def __str__(self): - """str override""" return force_unicode(': {0}').format(self.zone) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) diff --git a/dyn/tm/services/gslb.py b/dyn/tm/services/gslb.py index c2679ba..a77c733 100644 --- a/dyn/tm/services/gslb.py +++ b/dyn/tm/services/gslb.py @@ -81,15 +81,9 @@ def to_json(self): return output def __str__(self): - """str override""" return force_unicode(': {0}'.format( self.region_code) ) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) class GSLBRegion(APIObject): @@ -185,13 +179,7 @@ def to_json(self): return output def __str__(self): - """str override""" return force_unicode(': {0}').format(self.region_code) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) class GSLB(APIService): @@ -343,10 +331,4 @@ def add_region(self, region_code, pool, serve_count=None, return new_region def __str__(self): - """str override""" return force_unicode(': {0}').format(self.fqdn) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) diff --git a/dyn/tm/services/reversedns.py b/dyn/tm/services/reversedns.py index a3a16fb..0713d5d 100644 --- a/dyn/tm/services/reversedns.py +++ b/dyn/tm/services/reversedns.py @@ -106,10 +106,4 @@ def deactivate(self): self._update(deactivate=True) def __str__(self): - """str override""" return force_unicode(': {0}').format(self.fqdn) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) diff --git a/dyn/tm/services/rttm.py b/dyn/tm/services/rttm.py index 56d96b1..0812eeb 100644 --- a/dyn/tm/services/rttm.py +++ b/dyn/tm/services/rttm.py @@ -4,9 +4,9 @@ from ._shared import BaseMonitor from ..utils import APIList, Active, unix_date from ..session import DynectSession -from ...core import (APIObject, ImmutableAttribute, StringAttribute, - ValidatedAttribute, IntegerAttribute, ClassAttribute, - ValidatedListAttribute, ListAttribute) +from ...core import (APIObject, APIService, ImmutableAttribute, + StringAttribute, ValidatedAttribute, IntegerAttribute, + ClassAttribute, ValidatedListAttribute, ListAttribute) from ...compat import force_unicode __author__ = 'jnappi' @@ -62,6 +62,7 @@ def __init__(self, address, label, weight, serve_mode, **kwargs): :param serve_mode: Sets the behavior of this particular record. Must be one of 'always', 'obey', 'remove', or 'no' """ + self._address = address super(RegionPoolEntry, self).__init__(address, label, weight, serve_mode, **kwargs) @@ -83,13 +84,7 @@ def to_json(self): return json_blob def __str__(self): - """str override""" return force_unicode(': {0}').format(self.address) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) class RTTMRegion(APIObject): @@ -186,8 +181,7 @@ def _build(self, data): data.pop('pool', None) super(RTTMRegion, self)._build(data) - @property - def _json(self): + def to_json(self): """Unpack this object and return it as a JSON blob""" json_blob = {'region_code': self.region_code, 'pool': [entry.to_json() for entry in self.pool]} @@ -208,27 +202,57 @@ def _json(self): return json_blob def __str__(self): - """str override""" return force_unicode(': {0}').format(self.region_code) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) -class RTTM(APIObject): +class RTTM(APIService): + """Real Time Traffic Management (RTTM) is a DynECT Managed DNS service, + which monitors all of your endpoints to detect the best-performing ones and + also auto-populates your regional pools using that information to provide + you with the lowest latency possible. Differing from GSLB, RTTM collects + real-time performance data on the load time of each of your endpoints, + rather than just routing your traffic to manually entered, static + configurations. + """ uri = '/RTTM/{zone}/{fqdn}/' session_type = DynectSession + + #: The zone that this :class:`RTTM` service is attached to zone = ImmutableAttribute('zone') + + #: The FQDN of the node that this :class:`RTTM` service is attached to fqdn = ImmutableAttribute('fqdn') + + #: Indicates whether or not the service should automatically come out of + #: failover when the IP addresses resume active status or if the service + #: should remain in failover until manually reset. Must be one of + #: :const:`True`, :const:`False`, 'Y', or 'N' auto_recover = ValidatedAttribute('auto_recover', validator=('Y', 'N')) + + #: Time To Live in seconds of records in the service. Must be less than + #: 1/2 of the :class:`RTTMMonitor`'s monitoring interval. Must be one of + #: 30, 60, 150, 300, or 450. ttl = ValidatedAttribute('ttl', validator=(30, 60, 150, 300, 450)) + + #: A list of the events which trigger notifications. Must be one of 'ip', + #: 'svc', or 'nosrv' notify_events = ValidatedListAttribute('notify_events', validator=('ip', 'svc', 'nosrv')) + + #: The Hostname or IP address of a server to receive syslog notifications + #: on monitoring events syslog_server = StringAttribute('syslog_server') + + #: The port where the remote syslog server listens for notifications syslog_port = IntegerAttribute('syslog_port') + + #: The ident to use when sending syslog notifications syslog_ident = StringAttribute('syslog_ident') + + #: The syslog facility to use when sending syslog notifications. Must be + #: one of kern, user, mail, daemon, auth, syslog, lpr, news, uucp, cron, + #: authpriv, ftp, ntp, security, console, local0, local1, local2, local3, + #: local4, local5, local6, or local7 syslog_facility = ValidatedAttribute('syslog_facility', validator=( 'kern', 'user', 'mail', 'daemon', @@ -239,9 +263,15 @@ class RTTM(APIObject): 'local3', 'local4', 'local5', 'local6', 'local7' )) - monitor = ClassAttribute('monitor', class_type=Monitor) + + #: The :class:`RTTMMonitor` for this service + monitor = ClassAttribute('monitor', class_type=RTTMMonitor) + + #: The :class:`RTTMPerformanceMonitor` for this service performance_monitor = ClassAttribute('performance_monitor', - class_type=PerformanceMonitor) + class_type=RTTMPerformanceMonitor) + + #: Name of contact to receive notifications contact_nickname = StringAttribute('contact_nickname') def __init__(self, zone, fqdn, *args, **kwargs): @@ -252,8 +282,8 @@ def __init__(self, zone, fqdn, *args, **kwargs): active status or if the service should remain in failover until manually reset. Must be one of 'Y' or 'N' :param ttl: Time To Live in seconds of records in the service. Must be - less than 1/2 of the Health Probe's monitoring interval. Must be one - of 30, 60, 150, 300, or 450. + less than 1/2 of the Health Probe's monitoring interval. Must be + one of 30, 60, 150, 300, or 450. :param notify_events: A list of the events which trigger notifications. Must be one of 'ip', 'svc', or 'nosrv' :param syslog_server: The Hostname or IP address of a server to receive @@ -267,11 +297,12 @@ def __init__(self, zone, fqdn, *args, **kwargs): console, local0, local1, local2, local3, local4, local5, local6, or local7 :param region: A list of :class:`RTTMRegion`'s - :param monitor: The :class:`Monitor` for this service + :param monitor: The :class:`RTTMMonitor` for this service :param performance_monitor: The performance monitor for the service :param contact_nickname: Name of contact to receive notifications """ self.uri = self.uri.format(zone=zone, fqdn=fqdn) + self._zone, self._fqdn = zone, fqdn super(RTTM, self).__init__(*args, **kwargs) self.region.uri = self.uri @@ -282,41 +313,24 @@ def _post(self, contact_nickname, performance_monitor, region, ttl=None, """Create a new RTTM Service on the DynECT System""" api_args = {'contact_nickname': contact_nickname, 'performance_monitor': performance_monitor.to_json(), - 'region': region.to_json()} - if auto_recover: - self.auto_recover = auto_recover - api_args['auto_recover'] = self.auto_recover - if ttl: - self.ttl = ttl - api_args['ttl'] = self.ttl - if notify_events: - self.notify_events = notify_events - api_args['notify_events'] = self.notify_events - if syslog_server: - api_args['syslog_server'] = syslog_server - if syslog_port: - api_args['syslog_port'] = syslog_port - if syslog_ident: - api_args['syslog_ident'] = syslog_ident - if syslog_facility: - self.syslog_facility = syslog_facility - api_args['syslog_facility'] = self.syslog_facility - if region: - api_args['region'] = [region._json for region in region] + 'region': [reg.to_json() for reg in region], 'ttl': ttl, + 'auto_recover': auto_recover, + 'notify_events': notify_events, + 'syslog_server': syslog_server, 'syslog_port': syslog_port, + 'syslog_ident': syslog_ident, + 'syslog_facility': syslog_facility} if monitor: api_args['monitor'] = monitor.to_json() if performance_monitor: api_args['performance_monitor'] = performance_monitor.to_json() - if contact_nickname: - api_args['contact_nickname'] = contact_nickname # API expects a CSV string, not a list if isinstance(self.notify_events, list): - api_args['notify_events'] = ', '.join(self.notify_events) + api_args['notify_events'] = ', '.join(notify_events) - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) - self._build(response['data']) + api_args = {api_args[k] for k in api_args if api_args[k] is not None} + resp = DynectSession.get_session().execute(self.uri, 'POST', api_args) + self._build(resp['data']) def _build(self, data): """Build the neccesary substructures under this :class:`RTTM`""" @@ -338,7 +352,7 @@ def _build(self, data): monitor = data.pop('monitor') proto = monitor.pop('protocol', None) inter = monitor.pop('interval', None) - self.monitor = Monitor(proto, inter, **monitor) + self.monitor = RTTMMonitor(proto, inter, **monitor) if 'performance_monitor' in data: if self.performance_monitor is not None: self.performance_monitor.zone = self.zone @@ -347,8 +361,8 @@ def _build(self, data): monitor = data.pop('performance_monitor') proto = monitor.pop('protocol', None) inter = monitor.pop('interval', None) - self.performance_monitor = PerformanceMonitor(proto, inter, - **monitor) + self.performance_monitor = RTTMPerformanceMonitor(proto, inter, + **monitor) if 'notify_events' in data: self._notify_events = [item.strip() for item in data.pop('notify_events').split(',')] @@ -394,14 +408,6 @@ def get_log_report(self, start_ts, end_ts=None): 'POST', api_args) return response['data'] - def activate(self): - """Activate this RTTM Service""" - self._update(activate=True) - - def deactivate(self): - """Deactivate this RTTM Service""" - self._update(deactivate=True) - def recover(self, recoverip=None, address=None): """Recovers the RTTM service or a specific node IP within the service """ @@ -409,36 +415,8 @@ def recover(self, recoverip=None, address=None): if recoverip: api_args['recoverip'] = recoverip api_args['address'] = address - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def active(self): - """Returns whether or not this :class:`RTTM` Service is currently - active. When setting directly, rather than using activate/deactivate - valid arguments are 'Y' or True to activate, or 'N' or False to - deactivate. Note: If your service is already active and you try to - activate it, nothing will happen. And vice versa for deactivation. - - :returns: An :class:`Active` object representing the current state of - this :class:`ReverseDNS` Service - """ - return self._active - @active.setter - def active(self, value): - deactivate = ('N', False) - activate = ('Y', True) - if value in deactivate and self.active: - self.deactivate() - elif value in activate and not self.active: - self.activate() + resp = DynectSession.get_session().execute(self.uri, 'PUT', api_args) + self._build(resp['data']) def __str__(self): - """str override""" return force_unicode(': {0}').format(self.fqdn) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) diff --git a/dyn/tm/zones.py b/dyn/tm/zones.py index 04fcd65..c0dd6a1 100644 --- a/dyn/tm/zones.py +++ b/dyn/tm/zones.py @@ -554,15 +554,8 @@ def get_qps(self, start_ts, end_ts=None, breakdown=None, hosts=None, return response['data'] def __str__(self): - """str override""" return force_unicode(': {0}').format(self.name) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) - class SecondaryZone(APIObject): """A class representing DynECT Secondary zones""" @@ -633,15 +626,8 @@ def retransfer(self): self._update(retransfer=True) def __str__(self): - """str override""" return force_unicode(': {0}').format(self.zone) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) - class Node(object): """Node object. Represents a valid fqdn node within a zone. It should be @@ -798,10 +784,4 @@ def delete(self): DynectSession.get_session().execute(uri, 'DELETE') def __str__(self): - """str override""" return force_unicode(': {0}').format(self.fqdn) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) From 2308a8fa46ed9b58b80f9517068db5349e270f02 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 25 Dec 2014 23:12:15 -0500 Subject: [PATCH 33/59] Removed obnoxious pip.reqs lib function calls --- setup.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 2de3500..da50cb3 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,16 @@ from distutils.core import setup from dyn import __version__ -from pip.req import parse_requirements - with open('README.rst') as f: readme = f.read() with open('HISTORY.rst') as f: history = f.read() with open('LICENSE') as f: license_file = f.read() +with open('requirements.txt') as f: + requires = [line.strip() for line in f if line.strip()] +with open('test-requirements.txt') as f: + tests_requires = [line.strip() for line in f if line.strip()] setup( name='dyn', @@ -26,6 +28,6 @@ 'Topic :: Internet :: Name Service (DNS)', 'Topic :: Software Development :: Libraries', ], - install_requires=[str(pkg.req) for pkg in parse_requirements('requirements.txt')], - tests_require=[str(pkg.req) for pkg in parse_requirements('test-requirements.txt')], + install_requires=requires, + tests_require=tests_requires, ) From 307f80a09477379f9afd922fbc835a629d9d5264 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 25 Dec 2014 23:13:18 -0500 Subject: [PATCH 34/59] Fix for DSF APILists --- dyn/tm/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dyn/tm/utils.py b/dyn/tm/utils.py index 5713743..b2f7d95 100644 --- a/dyn/tm/utils.py +++ b/dyn/tm/utils.py @@ -79,7 +79,7 @@ def __iadd__(self, other): def __build_args(self): """Convert this list into an API Args dict""" - my_list = [x._json for x in self if x is not None] + my_list = [x.to_json() for x in self if x is not None] return {self.name: my_list} def _update(self, api_args): From 81bafcd61fce38b0d6142f2f7bd15a298e204f44 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 25 Dec 2014 23:43:18 -0500 Subject: [PATCH 35/59] Reorganized record object dictionaries to only need to be defined once --- dyn/tm/records.py | 13 ++++++++++++- dyn/tm/zones.py | 37 +++++++++++-------------------------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/dyn/tm/records.py b/dyn/tm/records.py index 049d03d..6a83e72 100644 --- a/dyn/tm/records.py +++ b/dyn/tm/records.py @@ -14,7 +14,8 @@ 'DHCIDRecord', 'DNAMERecord', 'DNSKEYRecord', 'DSRecord', 'KEYRecord', 'KXRecord', 'LOCRecord', 'IPSECKEYRecord', 'MXRecord', 'NAPTRRecord', 'PTRRecord', 'PXRecord', 'NSAPRecord', 'RPRecord', - 'NSRecord', 'SOARecord', 'SPFRecord', 'SRVRecord', 'TXTRecord'] + 'NSRecord', 'SOARecord', 'SPFRecord', 'SRVRecord', 'TXTRecord', + 'RECORD_TYPES'] # noinspection PyMissingConstructor @@ -540,3 +541,13 @@ def rdata(self): guts = super(TXTRecord, self).rdata() shell = {'txt_rdata': guts} return shell + +RECORD_TYPES = { + '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, 'PX': PXRecord, 'NSAP': NSAPRecord, 'RP': RPRecord, + 'NS': NSRecord, 'SOA': SOARecord, 'SPF': SPFRecord, 'SRV': SRVRecord, + 'TXT': TXTRecord +} diff --git a/dyn/tm/zones.py b/dyn/tm/zones.py index c0dd6a1..8b59fef 100644 --- a/dyn/tm/zones.py +++ b/dyn/tm/zones.py @@ -6,11 +6,7 @@ from .utils import unix_date from .errors import DynectCreateError, DynectGetError -from .records import (ARecord, AAAARecord, CERTRecord, CNAMERecord, - DHCIDRecord, DNAMERecord, DNSKEYRecord, DSRecord, - KEYRecord, KXRecord, LOCRecord, IPSECKEYRecord, MXRecord, - NAPTRRecord, PTRRecord, PXRecord, NSAPRecord, RPRecord, - NSRecord, SOARecord, SPFRecord, SRVRecord, TXTRecord) +from .records import RECORD_TYPES from .session import DynectSession from .services import (ActiveFailover, DynamicDNS, DNSSEC, TrafficDirector, GSLB, ReverseDNS, RTTM) @@ -21,14 +17,6 @@ __author__ = 'jnappi' __all__ = ['get_all_zones', 'Zone', 'SecondaryZone', 'Node'] -RECS = {'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, 'PX': PXRecord, - 'NSAP': NSAPRecord, 'RP': RPRecord, 'NS': NSRecord, 'SOA': SOARecord, - 'SPF': SPFRecord, 'SRV': SRVRecord, 'TXT': TXTRecord} - def get_all_zones(): """Accessor function to retrieve a *list* of all @@ -62,7 +50,7 @@ def get_all_secondary_zones(): # noinspection PyUnresolvedReferences class Zone(APIObject): - """A class representing a DynECT Zone""" + """A class representing a DynECT Primary Zone""" #: Primary Zone URI uri = '/Zone/{zone_name}/' @@ -114,8 +102,7 @@ def __init__(self, name, *args, **kwargs): complete """ self.uri = self.uri.format(zone_name=name) - self.records = {} - self.services = {} + self.records, self.services = dict(), dict() self._contact = self._ttl = None super(Zone, self).__init__(*args, **kwargs) self._fqdn = name if name.endswith('.') else '{0}.'.format(name) @@ -206,8 +193,6 @@ def __poll_for_get(self, n_loops=10, xfer=False, xfer_master_ip=None): raise DynectCreateError(response['msgs']) elif response['data']['status'] in ok_labels: self._get() - else: - pass # Should never get here @property def __root_soa(self): @@ -289,7 +274,7 @@ def add_record(self, name=None, record_type='A', *args, **kwargs): """ fqdn = name + '.' + self.name + '.' if name else self.name + '.' # noinspection PyCallingNonCallable - rec = RECS[record_type](self.name, fqdn, *args, **kwargs) + rec = RECORD_TYPES[record_type](self.name, fqdn, *args, **kwargs) if record_type in self.records: self.records[record_type].append(rec) else: @@ -364,7 +349,7 @@ def get_all_records(self): records = {} for key, record_list in record_lists.items(): search = key.split('_')[0].upper() - constructor = RECS[search] + constructor = RECORD_TYPES[search] list_records = [] for record in record_list: del record['zone'] @@ -397,7 +382,7 @@ def get_all_records_by_type(self, record_type): 'PX': 'PXRecord', 'NSAP': 'NSAPRecord', 'RP': 'RPRecord', 'NS': 'NSRecord', 'SOA': 'SOARecord', 'SPF': 'SPFRecord', 'SRV': 'SRVRecord', 'TXT': 'TXTRecord'} - constructor = RECS[record_type] + constructor = RECORD_TYPES[record_type] uri = '/{0}/{1}/{2}/'.format(names[record_type], self.name, self.fqdn) api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) @@ -428,7 +413,7 @@ def get_any_records(self): records = {} for key, record_list in record_lists.items(): search = key.split('_')[0].upper() - constructor = RECS[search] + constructor = RECORD_TYPES[search] list_records = [] for record in record_list: del record['zone'] @@ -661,7 +646,7 @@ def add_record(self, record_type='A', *args, **kwargs): :param kwargs: Keyword arguments to pass to the Record constructor """ # noinspection PyCallingNonCallable - rec = RECS[record_type](self.zone, self.fqdn, *args, **kwargs) + rec = RECORD_TYPES[record_type](self.zone, self.fqdn, *args, **kwargs) if record_type in self.records: self.records[record_type].append(rec) else: @@ -702,7 +687,7 @@ def get_all_records(self): records = {} for key, record_list in record_lists.items(): search = key.split('_')[0].upper() - constructor = RECS[search] + constructor = RECORD_TYPES[search] list_records = [] for record in record_list: del record['zone'] @@ -735,7 +720,7 @@ def get_all_records_by_type(self, record_type): 'PX': 'PXRecord', 'NSAP': 'NSAPRecord', 'RP': 'RPRecord', 'NS': 'NSRecord', 'SOA': 'SOARecord', 'SPF': 'SPFRecord', 'SRV': 'SRVRecord', 'TXT': 'TXTRecord'} - constructor = RECS[record_type] + constructor = RECORD_TYPES[record_type] uri = '/{0}/{1}/{2}/'.format(names[record_type], self.zone, self.fqdn) api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) @@ -763,7 +748,7 @@ def get_any_records(self): records = {} for key, record_list in record_lists.items(): search = key.split('_')[0].upper() - constructor = RECS[search] + constructor = RECORD_TYPES[search] list_records = [] for record in record_list: del record['zone'] From 7a3c3dab093cb160eb8f14af3472e8d13587349d Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 25 Dec 2014 23:43:45 -0500 Subject: [PATCH 36/59] Cleaned up permissions reporting and some misc other pieces --- dyn/tm/session.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/dyn/tm/session.py b/dyn/tm/session.py index bc3126f..217084d 100644 --- a/dyn/tm/session.py +++ b/dyn/tm/session.py @@ -120,16 +120,10 @@ def user_permissions_report(self, user_name=None): :param user_name: The user whose permissions will be returned. Defaults to the current user """ - api_args = dict() - api_args['user_name'] = user_name or self.username + api_args = {'user_name': user_name or self.username} uri = '/UserPermissionReport/' response = self.execute(uri, 'POST', api_args) - permissions = [] - for key, val in response['data'].items(): - if key == 'allowed': - for permission in val: - permissions.append(permission['name']) - return permissions + return [perm['name'] for perm in response['data'].get('allowed', [])] @property def permissions(self): @@ -137,9 +131,6 @@ def permissions(self): if self._permissions is None: self._permissions = self.user_permissions_report() return self._permissions - @permissions.setter - def permissions(self, value): - pass def authenticate(self): """Authenticate to the DynectSession service with the provided @@ -159,7 +150,7 @@ def authenticate(self): def log_out(self): """Log the current session out from the DynECT API system""" - self.execute('/Session/', 'DELETE', {}) + self.execute('/Session/', 'DELETE') self.close_session() @property From 355a6463fe13233338617e7512e51f2af687f221 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 25 Dec 2014 23:44:13 -0500 Subject: [PATCH 37/59] DSF changes done. Still need to be tested and documented --- dyn/tm/services/dsf.py | 1351 ++++++++++------------------------------ 1 file changed, 331 insertions(+), 1020 deletions(-) diff --git a/dyn/tm/services/dsf.py b/dyn/tm/services/dsf.py index 5d77224..5e470cc 100644 --- a/dyn/tm/services/dsf.py +++ b/dyn/tm/services/dsf.py @@ -1,23 +1,26 @@ # -*- coding: utf-8 -*- -"""This module contains wrappers for interfacing with every element of a Traffic -Director (DSF) service. +"""This module contains wrappers for interfacing with every element of a +Traffic Director (DSF) service. """ from ..utils import APIList, Active from ..errors import DynectInvalidArgumentError from ..records import * from ..session import DynectSession +from ...core import (APIObject, ImmutableAttribute, StringAttribute, + IntegerAttribute, ValidatedAttribute, APIDescriptor, + BooleanAttribute, ListAttribute) from ...compat import force_unicode __author__ = 'jnappi' __all__ = ['get_all_dsf_services', 'get_all_dsf_monitors', 'DSFARecord', - 'DSFAAAARecord', 'DSFCERTRecord', 'DSFCNAMERecord', 'DSFDHCIDRecord', - 'DSFDNAMERecord', 'DSFDNSKEYRecord', 'DSFDSRecord', 'DSFKEYRecord', - 'DSFKXRecord', 'DSFLOCRecord', 'DSFIPSECKEYRecord', 'DSFMXRecord', - 'DSFNAPTRRecord', 'DSFPTRRecord', 'DSFPXRecord', 'DSFNSAPRecord', - 'DSFRPRecord', 'DSFNSRecord', 'DSFSPFRecord', 'DSFSRVRecord', - 'DSFTXTRecord', 'DSFRecordSet', 'DSFFailoverChain', - 'DSFResponsePool', 'DSFRuleset', 'DSFMonitorEndpoint', 'DSFMonitor', - 'TrafficDirector'] + 'DSFAAAARecord', 'DSFCERTRecord', 'DSFCNAMERecord', + 'DSFDHCIDRecord', 'DSFDNAMERecord', 'DSFDNSKEYRecord', + 'DSFDSRecord', 'DSFKEYRecord', 'DSFKXRecord', 'DSFLOCRecord', + 'DSFIPSECKEYRecord', 'DSFMXRecord', 'DSFNAPTRRecord', + 'DSFPTRRecord', 'DSFPXRecord', 'DSFNSAPRecord', 'DSFRPRecord', + 'DSFNSRecord', 'DSFSPFRecord', 'DSFSRVRecord', 'DSFTXTRecord', + 'DSFRecordSet', 'DSFFailoverChain', 'DSFResponsePool', 'DSFRuleset', + 'DSFMonitorEndpoint', 'DSFMonitor', 'TrafficDirector'] def get_all_dsf_services(): @@ -42,8 +45,21 @@ def get_all_dsf_monitors(): return mons -class _DSFRecord(object): - """docstring for _DSFRecord""" +class _DSFRecord(APIObject): + """Base type for all DSF Records""" + uri = '' + session_type = DynectSession + label = StringAttribute('label') + weight = IntegerAttribute('weight') + automation = ValidatedAttribute('automation', + validator=('auto', 'auto_down', 'manual')) + endpoints = APIDescriptor('endpoints') + endpoint_up_count = IntegerAttribute('endpoint_up_count') + eligible = BooleanAttribute('eligible') + + dsf_id = ImmutableAttribute('dsf_id') + record_set_id = ImmutableAttribute('record_set_id') + def __init__(self, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): """Create a :class:`_DSFRecord` object. @@ -58,55 +74,39 @@ def __init__(self, label=None, weight=1, automation='auto', endpoints=None, Record status to be 'up' :param eligible: Indicates whether or not the Record can be served """ - self.valid_automation = ('auto', 'auto_down', 'manual') - self._label = label - self._weight = weight - if automation not in self.valid_automation: - raise DynectInvalidArgumentError('automation', automation, - self.valid_automation) - self._automation = automation - self._endpoints = endpoints - self._endpoint_up_count = endpoint_up_count - self._eligible = eligible - self._dsf_id = self._record_set_id = self.uri = None - for key, val in kwargs.items(): - setattr(self, '_' + key, val) + if 'api' not in kwargs: + kwargs['api'] = False + super(_DSFRecord, self).__init__(label, weight, automation, endpoints, + endpoint_up_count, eligible, **kwargs) def _post(self, dsf_id, record_set_id): """Create a new :class:`DSFRecord` on the DynECT System :param dsf_id: The unique system id for the DSF service associated with this :class:`DSFRecord` - :param record_set_id: The unique system id for the record set associated - with this :class:`DSFRecord` + :param record_set_id: The unique system id for the record set + associated with this :class:`DSFRecord` """ - self._dsf_id = dsf_id - self._record_set_id = record_set_id - self.uri = '/DSFRecord/{}/{}/'.format(self._dsf_id, self._record_set_id) + self.uri = '/DSFRecord/{0}/{1}/'.format(dsf_id, record_set_id) api_args = {} for key, val in self.__dict__.items(): if val is not None and not hasattr(val, '__call__') and \ key.startswith('_'): if key != '_dsf_id' and key != '_record_set_id': api_args[key[1:]] = val - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) - self._build(response['data']) + resp = DynectSession.get_session().execute(self.uri, 'POST', api_args) + self._build(resp['data']) def _get(self, dsf_id, record_set_id): """Get an existing :class:`DSFRecord` from the DynECT System :param dsf_id: The unique system id for the DSF service associated with this :class:`DSFRecord` - :param record_set_id: The unique system id for the record set associated - with this :class:`DSFRecord` + :param record_set_id: The unique system id for the record set + associated with this :class:`DSFRecord` """ - self._dsf_id = dsf_id - self._record_set_id = record_set_id - self.uri = '/DSFRecord/{}/{}/'.format(self._dsf_id, self._record_set_id) - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) + self.uri = '/DSFRecord/{0}/{1}/'.format(dsf_id, record_set_id) + response = DynectSession.get_session().execute(self.uri, 'GET') self._build(response['data']) def _build(self, data): @@ -120,112 +120,14 @@ def _build(self, data): else: setattr(self, '_' + key, val) - @property - def dsf_id(self): - """The unique system id for the DSF service associated with this - :class:`DSFRecord` - """ - return self._dsf_id - @dsf_id.setter - def dsf_id(self, value): - pass - - @property - def record_set_id(self): - """The unique system id for the record set associated with this - :class:`DSFRecord` - """ - return self._record_set_id - @record_set_id.setter - def record_set_id(self, value): - pass - - @property - def label(self): - """A unique label for this :class:`DSFRecord`""" - return self._label - @label.setter - def label(self, value): - self._label = value - api_args = {'label': self._label} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def weight(self): - """Weight for this :class:`DSFRecord`""" - return self._weight - @weight.setter - def weight(self, value): - self._weight = value - api_args = {'weight': self._weight} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def automation(self): - """Defines how eligible can be changed in response to monitoring. Must - be one of 'auto', 'auto_down', or 'manual' - """ - return self._automation - @automation.setter - def automation(self, value): - self._automation = value - api_args = {'automation': self._automation} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def endpoints(self): - """Endpoints are used to determine status, torpidity, and eligible in - response to monitor data - """ - return self._endpoints - @endpoints.setter - def endpoints(self, value): - self._endpoints = value - api_args = {'endpoints': self._endpoints} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def endpoint_up_count(self): - """Number of endpoints that must be up for the Record status to be 'up' - """ - return self._endpoint_up_count - @endpoint_up_count.setter - def endpoint_up_count(self, value): - self._endpoint_up_count = value - api_args = {'endpoint_up_count': self._endpoint_up_count} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def eligible(self): - """Indicates whether or not the Record can be served""" - return self._eligible - @eligible.setter - def eligible(self, value): - self._eligible = value - api_args = {'eligible': self._eligible} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - def to_json(self): """Convert this DSFRecord to a json blob""" - json = {'label': self._label, 'weight': self._weight, - 'automation': self._automation, 'endpoints': self._endpoints, - 'eligible': self._eligible, - 'endpoint_up_count': self._endpoint_up_count} + json = {'label': self.label, 'weight': self.weight, + 'automation': self.automation, 'endpoints': self.endpoints, + 'eligible': self.eligible, + 'endpoint_up_count': self.endpoint_up_count} json_blob = {x: json[x] for x in json if json[x] is not None} - if hasattr(self, '_record_type'): - # label = self._record_type.split('Record')[0].lower() + '_rdata' + if hasattr(self, '_record_type') and hasattr(self, 'rdata'): # We don't need to worry about rdata() throwing an error since if # we have a record type, then we know we're a subclass of a # DNSRecord @@ -266,8 +168,7 @@ def __init__(self, address, ttl=0, label=None, weight=1, automation='auto', Record status to be 'up' :param eligible: Indicates whether or not the Record can be served """ - ARecord.__init__(self, None, None, address=address, ttl=ttl, - create=False) + ARecord.__init__(self, None, None, address=address, ttl=ttl, api=False) _DSFRecord.__init__(self, label, weight, automation, endpoints, endpoint_up_count, eligible, **kwargs) @@ -332,8 +233,8 @@ def __init__(self, format, tag, algorithm, certificate, ttl=0, label=None, class DSFCNAMERecord(_DSFRecord, CNAMERecord): - """An :class:`CNAMERecord` object which is able to store additional data for - use by a :class:`TrafficDirector` service. + """An :class:`CNAMERecord` object which is able to store additional data + for use by a :class:`TrafficDirector` service. """ def __init__(self, cname, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, @@ -359,8 +260,8 @@ def __init__(self, cname, ttl=0, label=None, weight=1, automation='auto', class DSFDHCIDRecord(_DSFRecord, DHCIDRecord): - """An :class:`DHCIDRecord` object which is able to store additional data for - use by a :class:`TrafficDirector` service. + """An :class:`DHCIDRecord` object which is able to store additional data + for use by a :class:`TrafficDirector` service. """ def __init__(self, digest, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, @@ -386,8 +287,8 @@ def __init__(self, digest, ttl=0, label=None, weight=1, automation='auto', class DSFDNAMERecord(_DSFRecord, DNAMERecord): - """An :class:`DNAMERecord` object which is able to store additional data for - use by a :class:`TrafficDirector` service. + """An :class:`DNAMERecord` object which is able to store additional data + for use by a :class:`TrafficDirector` service. """ def __init__(self, dname, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, @@ -592,8 +493,9 @@ class DSFIPSECKEYRecord(_DSFRecord, IPSECKEYRecord): for use by a :class:`TrafficDirector` service. """ def __init__(self, precedence, gatetype, algorithm, gateway, public_key, - ttl=0, label=None, weight=1, automation='auto', endpoints=None, - endpoint_up_count=None, eligible=True, **kwargs): + ttl=0, label=None, weight=1, automation='auto', + endpoints=None, endpoint_up_count=None, eligible=True, + **kwargs): """Create a :class:`DSFIPSECKEYRecord` object :param precedence: Indicates priority among multiple IPSECKEYS. Lower @@ -618,8 +520,8 @@ def __init__(self, precedence, gatetype, algorithm, gateway, public_key, """ IPSECKEYRecord.__init__(self, None, None, precedence=precedence, gatetype=gatetype, algorithm=algorithm, - gateway=gateway, public_key=public_key, ttl=ttl, - create=False) + gateway=gateway, public_key=public_key, + ttl=ttl, create=False) _DSFRecord.__init__(self, label, weight, automation, endpoints, endpoint_up_count, eligible, **kwargs) @@ -700,9 +602,9 @@ class DSFPTRRecord(_DSFRecord, PTRRecord): """An :class:`PTRRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ - def __init__(self, ptrdname, ttl=0, label=None, weight=1, automation='auto', - endpoints=None, endpoint_up_count=None, eligible=True, - **kwargs): + def __init__(self, ptrdname, ttl=0, label=None, weight=1, + automation='auto', endpoints=None, endpoint_up_count=None, + eligible=True, **kwargs): """Create a :class:`DSFPTRRecord` object :param ptrdname: The hostname where the IP address should be directed @@ -727,9 +629,9 @@ class DSFPXRecord(_DSFRecord, PXRecord): """An :class:`PXRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ - def __init__(self, preference, map822, mapx400, ttl=0, label=None, weight=1, - automation='auto', endpoints=None, endpoint_up_count=None, - eligible=True, **kwargs): + def __init__(self, preference, map822, mapx400, ttl=0, label=None, + weight=1, automation='auto', endpoints=None, + endpoint_up_count=None, eligible=True, **kwargs): """Create a :class:`DSFPXRecord` object :param preference: Sets priority for processing records of the same @@ -748,7 +650,8 @@ def __init__(self, preference, map822, mapx400, ttl=0, label=None, weight=1, :param eligible: Indicates whether or not the Record can be served """ PXRecord.__init__(self, None, None, preference=preference, - map822=map822, mapx400=mapx400, ttl=ttl, create=False) + map822=map822, mapx400=mapx400, ttl=ttl, + create=False) _DSFRecord.__init__(self, label, weight, automation, endpoints, endpoint_up_count, eligible, **kwargs) @@ -879,9 +782,9 @@ def __init__(self, port, priority, target, rr_weight, ttl=0, label=None, exist on the zone/node :param target: The domain name of a host where the service is running on the specified port - :param rr_weight: Secondary prioritizing of records to serve. Records of - equal priority should be served based on their weight. Higher values - are served more often + :param rr_weight: Secondary prioritizing of records to serve. Records + of equal priority should be served based on their weight. Higher + values are served more often :param ttl: TTL for this record :param label: A unique label for this :class:`DSFSRVRecord` :param weight: Weight for this :class:`DSFSRVRecord` @@ -928,10 +831,24 @@ def __init__(self, txtdata, ttl=0, label=None, weight=1, automation='auto', endpoint_up_count, eligible, **kwargs) -class DSFRecordSet(object): +class DSFRecordSet(APIObject): """A Collection of DSFRecord Type objects belonging to a :class:`DSFFailoverChain` """ + session_type = DynectSession + records = ListAttribute('records') + status = ImmutableAttribute('status') + label = StringAttribute('label') + rdata_class = StringAttribute('rdata_class') + ttl = IntegerAttribute('ttl') + automation = ValidatedAttribute('automation', + validator=('auto', 'auto_down', 'manual')) + serve_count = IntegerAttribute('serve_count') + fail_count = IntegerAttribute('fail_count') + trouble_count = IntegerAttribute('trouble_count') + eligible = BooleanAttribute('eligible') + dsf_monitor_id = ImmutableAttribute('dsf_monitor_id') + def __init__(self, rdata_class, label=None, ttl=None, automation=None, serve_count=None, fail_count=None, trouble_count=None, eligible=None, dsf_monitor_id=None, records=None, **kwargs): @@ -948,10 +865,10 @@ def __init__(self, rdata_class, label=None, ttl=None, automation=None, :class:`DSFRecordSet` :param fail_count: The number of Records that must not be okay before this :class:`DSFRecordSet` becomes ineligible. - :param trouble_count: The number of Records that must not be okay before - this :class:`DSFRecordSet` becomes in trouble. - :param eligible: Indicates whether or not this :class:`DSFRecordSet` can - be served. + :param trouble_count: The number of Records that must not be okay + before this :class:`DSFRecordSet` becomes in trouble. + :param eligible: Indicates whether or not this :class:`DSFRecordSet` + can be served. :param dsf_monitor_id: The unique system id of the DSF Monitor attached to this :class:`DSFRecordSet` :param records: A list of :class:`DSFRecord`'s within this @@ -959,16 +876,15 @@ def __init__(self, rdata_class, label=None, ttl=None, automation=None, :param kwargs: Used for manipulating additional data to be speicified by the creation of other system objects. """ - super(DSFRecordSet, self).__init__() - self._label = label self._rdata_class = rdata_class - self._ttl = ttl - self._automation = automation - self._serve_count = serve_count - self._fail_count = fail_count - self._trouble_count = trouble_count - self._eligible = eligible - self._dsf_monitor_id = dsf_monitor_id + super(DSFRecordSet, self).__init__(rdata_class, label=label, ttl=ttl, + automation=automation, + serve_count=serve_count, + fail_count=fail_count, + trouble_count=trouble_count, + eligible=eligible, + dsf_monitor_id=dsf_monitor_id, + records=records, **kwargs) if records is not None and len(records) > 0 and isinstance(records[0], dict): self._records = [] @@ -988,7 +904,7 @@ def __init__(self, rdata_class, label=None, ttl=None, automation=None, 'srv': DSFSRVRecord, 'txt': DSFTXTRecord} rec_type = record['rdata_class'].lower() constructor = constructors[rec_type] - rdata_key = 'rdata_{}'.format(rec_type) + rdata_key = 'rdata_{0}'.format(rec_type) kws = ('ttl', 'label', 'weight', 'automation', 'endpoints', 'endpoint_up_count', 'eligible', 'dsf_record_id', 'dsf_record_set_id', 'status', 'torpidity') @@ -1007,9 +923,10 @@ def __init__(self, rdata_class, label=None, ttl=None, automation=None, if key != 'records': setattr(self, '_' + key, val) # If dsf_id and dsf_response_pool_id were specified in kwargs - if self._service_id is not None and self._dsf_record_set_id is not None: - self.uri = '/DSFRecordSet/{}/{}/'.format(self._service_id, - self._dsf_record_set_id) + if self._service_id is not None \ + and self._dsf_record_set_id is not None: + self.uri = '/DSFRecordSet/{0}/{1}/'.format(self._service_id, + self._dsf_record_set_id) def _post(self, dsf_id): """Create a new :class:`DSFRecordSet` on the DynECT System @@ -1018,7 +935,7 @@ def _post(self, dsf_id): :class:`DSFRecordSet` is attached to """ self._service_id = dsf_id - uri = '/DSFRecordSet/{}/'.format(self._service_id) + uri = '/DSFRecordSet/{0}/'.format(self._service_id) api_args = {} for key, val in self.__dict__.items(): if key == 'records': @@ -1028,8 +945,8 @@ def _post(self, dsf_id): api_args[key[1:]] = val response = DynectSession.get_session().execute(uri, 'POST', api_args) self._build(response['data']) - self.uri = '/DSFRecordSet/{}/{}/'.format(self._service_id, - self._dsf_record_set_id) + self.uri = '/DSFRecordSet/{0}/{1}/'.format(self._service_id, + self._dsf_record_set_id) def _get(self, dsf_id, dsf_record_set_id): """Get an existing :class:`DSFRecordSet` from the DynECT System @@ -1041,11 +958,9 @@ def _get(self, dsf_id, dsf_record_set_id): """ self._service_id = dsf_id self._dsf_record_set_id = dsf_record_set_id - self.uri = '/DSFRecordSet/{}/{}/'.format(self._service_id, - self._dsf_record_set_id) - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) + self.uri = '/DSFRecordSet/{0}/{1}/'.format(self._service_id, + self._dsf_record_set_id) + response = DynectSession.get_session().execute(self.uri, 'GET') self._build(response['data']) def _build(self, data): @@ -1054,197 +969,37 @@ def _build(self, data): if key != 'records': setattr(self, '_' + key, val) - @property - def records(self): - """The list of DSFRecord types that are stored in this - :class:`DSFRecordSet` - """ - return self._records - @records.setter - def records(self, value): - pass - - @property - def status(self): - """The current status of this :class:`DSFRecordSet`""" - self._get(self._service_id, self._dsf_record_set_id) - return self._status - @status.setter - def status(self, value): - pass - - @property - def label(self): - """A unique label for this :class:`DSFRecordSet`""" - return self._label - @label.setter - def label(self, value): - self._label = value - api_args = {'label': self._label} - if self._master_line: - api_args['master_line'] = self._master_line - else: - api_args['rdata'] = self._rdata - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def rdata_class(self): - """The rdata property is a read-only attribute""" - return self._rdata_class - @rdata_class.setter - def rdata_class(self, value): - pass - - @property - def ttl(self): - """Default TTL for :class:`DSFRecord`'s within this - :class:`DSFRecordSet`""" - return self._ttl - @ttl.setter - def ttl(self, value): - self._ttl = value - api_args = {'ttl': self._ttl} - if self._master_line: - api_args['master_line'] = self._master_line - else: - api_args['rdata'] = self._rdata - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def automation(self): - """Defines how eligible can be changed in response to monitoring""" - return self._automation - @automation.setter - def automation(self, value): - self._automation = value - api_args = {'automation': self._automation} - if self._master_line: - api_args['master_line'] = self._master_line - else: - api_args['rdata'] = self._rdata - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def serve_count(self): - """How many Records to serve out of this :class:`DSFRecordSet`""" - return self._serve_count - @serve_count.setter - def serve_count(self, value): - self._serve_count = value - api_args = {'serve_count': self._serve_count} - if self._master_line: - api_args['master_line'] = self._master_line - else: - api_args['rdata'] = self._rdata - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def fail_count(self): - """The number of Records that must not be okay before this - :class:`DSFRecordSet` becomes ineligible. - """ - return self._fail_count - @fail_count.setter - def fail_count(self, value): - self._fail_count = value - api_args = {'fail_count': self._fail_count} - if self._master_line: - api_args['master_line'] = self._master_line - else: - api_args['rdata'] = self._rdata - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def trouble_count(self): - """The number of Records that must not be okay before this - :class:`DSFRecordSet` becomes in trouble. - """ - return self._trouble_count - @trouble_count.setter - def trouble_count(self, value): - self._trouble_count = value - api_args = {'trouble_count': self._trouble_count} - if self._master_line: - api_args['master_line'] = self._master_line - else: - api_args['rdata'] = self._rdata - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def eligible(self): - """Indicates whether or not this :class:`DSFRecordSet` can be served.""" - return self._eligible - @eligible.setter - def eligible(self, value): - self._eligible = value - api_args = {'eligible': self._eligible} - if self._master_line: - api_args['master_line'] = self._master_line - else: - api_args['rdata'] = self._rdata - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - - @property - def dsf_monitor_id(self): - """The unique system id of the DSF Monitor attached to this - :class:`DSFRecordSet` - """ - return self._dsf_monitor_id - @dsf_monitor_id.setter - def dsf_monitor_id(self, value): - self._dsf_monitor_id = value - api_args = {'dsf_monitor_id': self._dsf_monitor_id} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) - def to_json(self): """Convert this :class:`DSFRecordSet` to a JSON blob""" - json_blob = {'rdata_class': self._rdata_class} - if self._label: - json_blob['label'] = self._label - if self._ttl: - json_blob['ttl'] = self._ttl - if self._automation: - json_blob['automation'] = self._automation - if self._serve_count: - json_blob['serve_count'] = self._serve_count - if self._fail_count: - json_blob['fail_count'] = self._fail_count - if self._trouble_count: - json_blob['trouble_count'] = self._trouble_count - if self._eligible: - json_blob['eligible'] = self._eligible - if self._dsf_monitor_id: - json_blob['dsf_monitor_id'] = self._dsf_monitor_id - if self._records: - json_blob['records'] = [rec.to_json() for rec in self._records] - else: - json_blob['records'] = [] - return json_blob + json_blob = {'rdata_class': self._rdata_class, + 'label': self.label, 'ttl': self.ttl, + 'automation': self.automation, + 'serve_count': self.serve_count, + 'fail_count': self.fail_count, + 'trouble_count': self.trouble_count, + 'eligible': self.eligible, + 'dsf_monitor_id': self.dsf_monitor_id, + 'records': [rec.to_json() for rec in self.records]} + return {json_blob[key] for key in json_blob if json_blob[key]} def delete(self): """Delete this :class:`DSFRecordSet` from the Dynect System""" api_args = {'publish': 'Y'} DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + def __str__(self): + return ': {1}'.format(self.rdata_class, self.label) + + +class DSFFailoverChain(APIObject): + uri = '/DSFRecordSetFailoverChain/{0}/{1}/' + session_type = DynectSession + label = StringAttribute('label') + core = BooleanAttribute('core') + record_sets = ListAttribute('record_sets') + dsf_response_pool_id = ImmutableAttribute('dsf_response_pool_id') + dsf_id = ImmutableAttribute('dsf_id') -class DSFFailoverChain(object): - """docstring for DSFFailoverChain""" def __init__(self, label=None, core=None, record_sets=None, **kwargs): """Create a :class:`DSFFailoverChain` object @@ -1254,7 +1009,6 @@ def __init__(self, label=None, core=None, record_sets=None, **kwargs): :param record_sets: A list of :class:`DSFRecordSet`'s for this :class:`DSFFailoverChain` """ - super(DSFFailoverChain, self).__init__() self._label = label self._core = core if isinstance(record_sets, list) and len(record_sets) > 0 and \ @@ -1263,20 +1017,13 @@ def __init__(self, label=None, core=None, record_sets=None, **kwargs): self._record_sets = [] # Create new record set objects for record_set in record_sets: - if 'service_id' in record_set and \ - record_set['service_id'] == '': + if 'service_id' in record_set \ + and record_set['service_id'] == '': record_set['service_id'] = kwargs['service_id'] - self._record_sets.append(DSFRecordSet(**record_set)) + self._record_sets.append(DSFRecordSet(api=False, **record_set)) else: self._record_sets = record_sets - self._dsf_id = self._dsf_response_pool_id = self.uri = None - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - # If dsf_id and dsf_response_pool_id were specified in kwargs - if self._dsf_id is not None and self._dsf_response_pool_id is not None: - r_pid = self._dsf_response_pool_id - self.uri = '/DSFRecordSetFailoverChain/{}/{}/'.format(self._dsf_id, - r_pid) + super(DSFFailoverChain, self).__init__(**kwargs) def _post(self, dsf_id, dsf_response_pool_id): """Create a new :class:`DSFFailoverChain` on the Dynect System @@ -1288,8 +1035,7 @@ def _post(self, dsf_id, dsf_response_pool_id): """ self._dsf_id = dsf_id self._dsf_response_pool_id = dsf_response_pool_id - self.uri = '/DSFRecordSetFailoverChain/{}/{}/'.format(self._dsf_id, - self._dsf_response_pool_id) + self.uri = self.uri.format(self.dsf_id, self.dsf_response_pool_id) api_args = {'publish': 'Y'} if self._label: api_args['label'] = self._label @@ -1297,11 +1043,8 @@ def _post(self, dsf_id, dsf_response_pool_id): api_args['core'] = self._core if self._record_sets: api_args['record_sets'] = self._record_sets.to_json() - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) + resp = DynectSession.get_session().execute(self.uri, 'POST', api_args) + self._build(resp['data']) def _get(self, dsf_id, dsf_response_pool_id): """Retrieve an existing :class:`DSFFailoverChain` from the Dynect System @@ -1313,64 +1056,24 @@ def _get(self, dsf_id, dsf_response_pool_id): """ self._dsf_id = dsf_id self._dsf_response_pool_id = dsf_response_pool_id - self.uri = '/DSFRecordSetFailoverChain/{}/{}/'.format(self._dsf_id, - self._dsf_response_pool_id) - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) - - @property - def label(self): - """A unique label for this :class:`DSFFailoverChain`""" - return self._label - @label.setter - def label(self, value): - self._label = value - api_args = {'label': self._label} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) + self.uri = self.uri.format(self.dsf_id, self.dsf_response_pool_id) + super(DSFFailoverChain, self)._get() - @property - def core(self): - """Indicates whether or not the contained :class:`DSFRecordSet`'s are - core record sets. - """ - return self._core - @core.setter - def core(self, value): - self._core = value - api_args = {'core': self._core} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) - - @property - def record_sets(self): - """A list of hashes to create a new :class:`DSFRecordSet` or - specify/update an existing :class:`DSFRecordSet` - """ - return self._record_sets - @record_sets.setter - def record_sets(self, value): - pass + def _build(self, data): + if 'record_sets' in data: + data.pop('record_sets') + super(DSFFailoverChain, self)._build(data) def to_json(self): - """Convert this :class:`DSFFailoverChain` to a JSON blob""" + """Convert this :class:`DSFFailoverChain` to JSON""" json_blob = {} if self._label: json_blob['label'] = self._label if self._core: json_blob['core'] = self._core if self.record_sets: - json_blob['record_sets'] = [rs.to_json() for rs in self.record_sets] + json_blob['record_sets'] = [rs.to_json() for rs in + self.record_sets] return json_blob def delete(self): @@ -1378,10 +1081,25 @@ def delete(self): api_args = {'publish': 'Y'} DynectSession.get_session().execute(self.uri, 'DELETE', api_args) - -class DSFResponsePool(object): - """docstring for DSFResponsePool""" - def __init__(self, label, core_set_count=1, eligible=True, + def __str__(self): + return ': {0}'.format(self.label) + + +class DSFResponsePool(APIObject): + uri = '/DSFResponsePool/{0}/{1}/' + session_type = DynectSession + _get_length = 1 + label = StringAttribute('label') + core_set_count = IntegerAttribute('core_set_count') + eligible = BooleanAttribute('eligible') + automation = StringAttribute('automation') + dsf_ruleset_id = ImmutableAttribute('dsf_ruleset_id') + dsf_response_pool_id = ImmutableAttribute('dsf_response_pool_id') + index = IntegerAttribute('index') + rs_chains = ImmutableAttribute('rs_chains') + dsf_id = ImmutableAttribute('dsf_id') + + def __init__(self, label, dsf_id, core_set_count=1, eligible=True, automation='auto', dsf_ruleset_id=None, index=None, rs_chains=None, **kwargs): """Create a :class:`DSFResponsePool` object @@ -1400,168 +1118,63 @@ def __init__(self, label, core_set_count=1, eligible=True, :param rs_chains: A list of :class:`DSFFailoverChain` that are defined for this :class:`DSFResponsePool` """ - super(DSFResponsePool, self).__init__() - self._label = label - self._core_set_count = core_set_count - self._eligible = eligible - self._automation = automation - self._dsf_ruleset_id = dsf_ruleset_id - self._index = index + self._label, self._dsf_id = label, dsf_id + if isinstance(rs_chains, list) and len(rs_chains) > 0 and \ isinstance(rs_chains[0], dict): # Clear Failover Chains self._rs_chains = [] # Create a new FO Chain for each entry returned from API for chain in rs_chains: - self._rs_chains.append(DSFFailoverChain(**chain)) + self._rs_chains.append(DSFFailoverChain(api=False, **chain)) else: self._rs_chains = rs_chains - self._dsf_id = self._dsf_response_pool_id = self.uri = None - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - # If dsf_id and dsf_response_pool_id were specified in kwargs - if self._dsf_id is not None and self._dsf_response_pool_id is not None: - r_pid = self._dsf_response_pool_id - self.uri = '/DSFResponsePool/{}/{}/'.format(self._dsf_id, - r_pid) - def _post(self, dsf_id): - """Create a new :class:`DSFResponsePool` on the DynECT System + super(DSFResponsePool, self).__init__(core_set_count=core_set_count, + eligible=eligible, + automation=automation, + dsf_ruleset_id=dsf_ruleset_id, + index=index, **kwargs) - :param dsf_id: the id of the DSF service this :class:`DSFResponsePool` - is attached to - """ - self._dsf_id = dsf_id - uri = '/DSFResponsePool/{}/'.format(self._dsf_id) + def _post(self): + """Create a new :class:`DSFResponsePool` on the DynECT System""" api_args = {'publish': 'Y', 'label': self._label, - 'core_set_count': self._core_set_count, - 'eligible': self._eligible, 'automation': self._automation} - if self._dsf_ruleset_id: - api_args['dsf_ruleset_id'] = self._dsf_ruleset_id - if self._index: - api_args['index'] = self._index + 'core_set_count': self.core_set_count, + 'eligible': self.eligible, 'automation': self.automation} + if self.dsf_ruleset_id: + api_args['dsf_ruleset_id'] = self.dsf_ruleset_id + if self.index: + api_args['index'] = self.index if self._rs_chains: api_args['rs_chains'] = self._rs_chains - response = DynectSession.get_session().execute(uri, 'POST', api_args) - for key, val in response['data'].items(): - if key != 'rs_chains': - setattr(self, '_' + key, val) - self.uri = '/DSFResponsePool/{}/{}/'.format(self._dsf_id, - self._dsf_response_pool_id) + resp = DynectSession.get_session().execute(self.uri, 'POST', api_args) + self._build(resp['data']) - def _get(self, dsf_id, dsf_response_pool_id): + def _get(self, dsf_response_pool_id): """Get an existing :class:`DSFResponsePool` from the DynECT System - :param dsf_id: the id of the DSF service this :class:`DSFResponsePool` - is attached to :param dsf_response_pool_id: the id of this :class:`DSFResponsePool` """ - self._dsf_id = dsf_id - self._dsf_response_pool_id = dsf_response_pool_id - self.uri = '/DSFResponsePool/{}/{}/'.format(self._dsf_id, - self._dsf_response_pool_id) - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - for key, val in response['data'].items(): - if key != 'rs_chains': - setattr(self, '_' + key, val) - - @property - def label(self): - """A unique label for this :class:`DSFResponsePool`""" - return self._label - @label.setter - def label(self, value): - self._label = value - api_args = {'label': self._label} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) - - @property - def core_set_count(self): - """If fewer than this number of core record sets are eligible, status - will be set to fail - """ - return self._core_set_count - @core_set_count.setter - def core_set_count(self, value): - self._core_set_count = value - api_args = {'core_set_count': self._core_set_count} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) - - @property - def eligible(self): - """Indicates whether or not the :class:`DSFResponsePool` can be served - """ - return self._eligible - @eligible.setter - def eligible(self, value): - self._eligible = value - api_args = {'eligible': self._eligible} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) - - @property - def automation(self): - """Defines how eligible can be changed in response to monitoring""" - return self._automation - @automation.setter - def automation(self, value): - self._automation = value - api_args = {'automation': self._automation} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) - - @property - def dsf_ruleset_id(self): - """Unique system id of the Ruleset this :class:`DSFResponsePool` is - attached to - """ - return self._dsf_ruleset_id - @dsf_ruleset_id.setter - def dsf_ruleset_id(self, value): - self._dsf_ruleset_id = value - api_args = {'dsf_ruleset_id': self._dsf_ruleset_id} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) + self.uri = self.uri.format(self.dsf_id, dsf_response_pool_id) + response = DynectSession.get_session().execute(self.uri, 'GET') + self._build(response['data']) - @property - def rs_chains(self): - """A list of :class:`DSFFailoverChain` that are defined for this - :class:`DSFResponsePool` - """ - return self._rs_chains - @rs_chains.setter - def rs_chains(self, value): - pass + def _build(self, data): + if 'rs_chains' in data: + data.pop('rs_chains') + super(DSFResponsePool, self)._build(data) + self.uri = self.uri.format(self.dsf_id, self.dsf_response_pool_id) def to_json(self): """Convert this :class:`DSFResponsePool` to a JSON blob""" rs_json = [rs.to_json() for rs in self._rs_chains] - json_blob = {'label': self._label, 'eligible': self._eligible, - 'core_set_count': self._core_set_count, - 'automation': self._automation, 'rs_chains': rs_json} - if self._index: - json_blob['index'] = self._index - if self._dsf_ruleset_id: - json_blob['dsf_ruleset_id'] = self._dsf_ruleset_id + json_blob = {'label': self.label, 'eligible': self.eligible, + 'core_set_count': self.core_set_count, + 'automation': self.automation, 'rs_chains': rs_json} + if self.index: + json_blob['index'] = self.index + if self.dsf_ruleset_id: + json_blob['dsf_ruleset_id'] = self.dsf_ruleset_id return json_blob def delete(self): @@ -1569,9 +1182,18 @@ def delete(self): api_args = {'publish': 'Y'} DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + def __str__(self): + return ': {0}'.format(self.dsf_response_pool_id) + + +class DSFRuleset(APIObject): + session_type = DynectSession + label = StringAttribute('label') + criteria_type = ValidatedAttribute('criteria_type', + validator=('always', 'geoip')) + criteria = ListAttribute('criteria') + response_pools = ImmutableAttribute('response_pools') -class DSFRuleset(object): - """docstring for DSFRuleset""" def __init__(self, label, criteria_type, response_pools, criteria=None, **kwargs): """Create a :class:`DSFRuleset` object @@ -1583,28 +1205,19 @@ def __init__(self, label, criteria_type, response_pools, criteria=None, :param response_pools: A list of :class:`DSFResponsePool`'s for this :class:`DSFRuleset` """ - super(DSFRuleset, self).__init__() - self.valid_criteria_types = ('always', 'geoip') - self.valid_criteria = {'always': (), - 'geoip': ()} - self._label = label - self._criteria_type = criteria_type + self._label, self._criteria_type = label, criteria_type self._criteria = criteria if isinstance(response_pools, list) and len(response_pools) > 0 and \ isinstance(response_pools[0], dict): self._response_pools = [] for pool in response_pools: pool = {x: pool[x] for x in pool if x != 'rulesets'} - self._response_pools.append(DSFResponsePool(**pool)) + self._response_pools.append(DSFResponsePool(api=False, **pool)) + kwargs['api'] = False else: self._response_pools = response_pools self._service_id = self._dsf_ruleset_id = self.uri = None - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - # If dsf_id and dsf_ruleset_id were specified in kwargs - if self._service_id is not None and self._dsf_ruleset_id is not None: - self.uri = '/DSFRuleset/{}/{}/'.format(self._service_id, - self._dsf_ruleset_id) + super(DSFRuleset, self).__init__(**kwargs) def _post(self, dsf_id): """Create a new :class:`DSFRuleset` on the DynECT System @@ -1613,16 +1226,14 @@ def _post(self, dsf_id): attached to """ self._service_id = dsf_id - uri = '/DSFRuleset/{}/'.format(self._service_id) + uri = '/DSFRuleset/{0}/'.format(self._service_id) api_args = {'publish': 'Y', 'label': self._label, 'criteria_type': self._criteria_type, 'criteria': self._criteria} response = DynectSession.get_session().execute(uri, 'POST', api_args) - for key, val in response['data'].items(): - if key != 'rs_chains': - setattr(self, '_' + key, val) - self.uri = '/DSFRuleset/{}/{}/'.format(self._service_id, - self._dsf_ruleset_id) + self._build(response['data']) + self.uri = '/DSFRuleset/{0}/{1}/'.format(self._service_id, + self._dsf_ruleset_id) def _get(self, dsf_id, dsf_ruleset_id): """Get an existing :class:`DSFRuleset` from the DynECT System @@ -1631,76 +1242,20 @@ def _get(self, dsf_id, dsf_ruleset_id): attached to :param dsf_ruleset_id: The unique system id of this :class:`DSFRuleset` """ - self._service_id = dsf_id - self._dsf_ruleset_id = dsf_ruleset_id - self.uri = '/DSFRuleset/{}/{}/'.format(self._service_id, - self._dsf_ruleset_id) - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - for key, val in response['data'].items(): - if key != 'rs_chains': - setattr(self, '_' + key, val) + self.uri = '/DSFRuleset/{0}/{1}/'.format(dsf_id, dsf_ruleset_id) + super(DSFRuleset, self)._get() - @property - def label(self): - """A unique label for this :class:`DSFRuleset`""" - return self._label - @label.setter - def label(self, value): - self._label = value - api_args = {'label': self._label} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) - - @property - def criteria_type(self): - """A set of rules describing what traffic is applied to the - :class:`DSFRuleset` - """ - return self._criteria_type - @criteria_type.setter - def criteria_type(self, value): - self._criteria_type = value - api_args = {'criteria_type': self._criteria_type} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) - - @property - def criteria(self): - """The criteria rules, will be varied depending on the specified - criteria_type - """ - return self._criteria - @criteria.setter - def criteria(self, value): - self._criteria = value - api_args = {'criteria': self._criteria} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key != 'record_sets': - setattr(self, '_' + key, val) - - @property - def response_pools(self): - """A list of :class:`DSFResponsePool`'s for this :class:`DSFRuleset`""" - return self._response_pools - @response_pools.setter - def response_pools(self, value): - pass + def _build(self, data): + """Pop out unused rs_chains data""" + if 'rs_chains' in data: + data.pop('rs_chains') + super(DSFRuleset, self)._build(data) - @property - def _json(self): + def to_json(self): """JSON-ified version of this DSFRuleset Object""" pool_json = [pool.to_json() for pool in self._response_pools] - json_blob = {'label': self._label, 'criteria_type': self._criteria_type, + json_blob = {'label': self._label, + 'criteria_type': self._criteria_type, 'criteria': self._criteria, 'response_pools': pool_json} return json_blob @@ -1712,6 +1267,9 @@ def delete(self): api_args = {'publish': 'Y'} DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + def __str__(self): + return ': {0}'.format(self.label) + class DSFMonitorEndpoint(object): """An Endpoint object to be passed to a :class:`DSFMonitor`""" @@ -1720,8 +1278,9 @@ def __init__(self, address, label, active='Y', site_prefs=None): :param address: The address to monitor. :param label: A label to identify this :class:`DSFMonitorEndpoint`. - :param active: Indicates whether or not this :class:`DSFMonitorEndpoint` - endpoint is active. Must be one of True, False, 'Y', or 'N' + :param active: Indicates whether or not this + :class:`DSFMonitorEndpoint` endpoint is active. Must be one of + True, False, 'Y', or 'N' :param site_prefs: A ``list`` of site codes from which this :class:`DSFMonitorEndpoint` will be monitored """ @@ -1743,12 +1302,11 @@ def _update(self, api_args): if id(endpoint) == id(self): args_list.append(api_args) else: - args_list.append(endpoint._json) + args_list.append(endpoint.to_json()) api_args = {'endpoints': args_list} self._monitor._update(api_args) - @property - def _json(self): + def to_json(self): """Get the JSON representation of this :class:`DSFMonitorEndpoint` object """ @@ -1772,7 +1330,7 @@ def active(self, value): valid_input = ('Y', 'N', True, False) if value not in valid_input: raise DynectInvalidArgumentError('active', value, valid_input) - api_args = self._json + api_args = self.to_json() api_args['active'] = value self._update(api_args) @@ -1781,7 +1339,7 @@ def label(self): return self._label @label.setter def label(self, value): - api_args = self._json + api_args = self.to_json() api_args['label'] = value self._update(api_args) @@ -1790,7 +1348,7 @@ def address(self): return self._address @address.setter def address(self, value): - api_args = self._json + api_args = self.to_json() api_args['address'] = value self._update(api_args) @@ -1799,235 +1357,108 @@ def site_prefs(self): return self._site_prefs @site_prefs.setter def site_prefs(self, value): - api_args = self._json + api_args = self.to_json() api_args['site_prefs'] = value self._update(api_args) -class DSFMonitor(object): +class DSFMonitor(APIObject): """A Monitor for a :class:`TrafficDirector` Service""" - def __init__(self, *args, **kwargs): - """Create a new :class:`DSFMonitor` object - - :param label: A unique label to identify this :class:`DSFMonitor` - :param protocol: The protocol to monitor. Must be one of 'HTTP', - 'HTTPS', 'PING', 'SMTP', or 'TCP' - :param response_count: The number of responses to determine whether or - not the endpoint is 'up' or 'down' - :param probe_interval: How often to run this :class:`DSFMonitor` - :param retries: How many retries this :class:`DSFMonitor` should attempt - on failure before giving up. - :param active: Indicates if this :class:`DSFMonitor` is active - :param options: Additional options pertaining to this - :class:`DSFMonitor` - :param endpoints: A List of :class:`DSFMonitorEndpoint`'s that are - associated with this :class:`DSFMonitor` - """ - super(DSFMonitor, self).__init__() - self.uri = None - self._monitor_id = None - self._label = self._protocol = self._response_count = None - self._probe_interval = self._retries = self._active = None - self._options = self._dsf_monitor_id = self._timeout = self._port = None - self._path = self._host = self._header = self._expected = None - self._endpoints = [] - if 'api' in kwargs: - del kwargs['api'] - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - self.uri = '/DSFMonitor/{}/'.format(self._dsf_monitor_id) - elif len(args) + len(kwargs) == 1: - self._get(*args, **kwargs) - else: - self._post(*args, **kwargs) + uri = '' + session_type = DynectSession + _get_length = 1 + dsf_monitor_id = ImmutableAttribute('dsf_monitor_id') + label = StringAttribute('label') + protocol = ValidatedAttribute('protocol', + validator=('HTTP', 'HTTPS', 'PING', 'SMTP', + 'TCP')) + response_count = IntegerAttribute('response_count') + probe_interval = IntegerAttribute('probe_interval') + retries = IntegerAttribute('retries') + active = StringAttribute('active') + options = APIDescriptor('options') + endpoints = ListAttribute('endpoints') def _get(self, monitor_id): """Get an existing :class:`DSFMonitor` from the DynECT System""" - self._monitor_id = monitor_id - self.uri = '/DSFMonitor/{}/'.format(self._monitor_id) - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - self._build(response['data']) + self.uri = '/DSFMonitor/{0}/'.format(monitor_id) + super(DSFMonitor, self)._get() def _post(self, label, protocol, response_count, probe_interval, retries, active='Y', timeout=None, port=None, path=None, host=None, header=None, expected=None, endpoints=None): """Create a new :class:`DSFMonitor` on the DynECT System""" uri = '/DSFMonitor/' - self._label = label - self._protocol = protocol - self._response_count = response_count - self._probe_interval = probe_interval - self._retries = retries self._active = Active(active) self._options = {} if timeout: - self._timeout = timeout self._options['timeout'] = timeout if port: - self._port = port self._options['port'] = port if path: - self._path = path self._options['path'] = path if host: - self._host = host self._options['host'] = host if header: - self._header = header self._options['header'] = header if expected: - self._expected = expected self._options['expected'] = expected - self._endpoints = endpoints - api_args = {'label': self._label, - 'protocol': self._protocol, - 'response_count': self._response_count, - 'probe_interval': self._probe_interval, - 'retries': self._retries, - 'active': str(self._active), + api_args = {'label': label, 'protocol': protocol, + 'response_count': response_count, + 'probe_interval': probe_interval, + 'retries': retries, 'active': str(self._active), 'options': self._options} if self._endpoints is not None: - api_args['endpoints'] = [x._json for x in self._endpoints] + api_args['endpoints'] = [x.to_json() for x in self._endpoints] response = DynectSession.get_session().execute(uri, 'POST', api_args) self._build(response['data']) - self.uri = '/DSFMonitor/{}/'.format(self._dsf_monitor_id) - - def _update(self, api_args): - """Private Update method""" - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) def _build(self, data): """Update this object based on the information passed in via data :param data: The ``['data']`` field from an API JSON response """ - for key, val in data.items(): - if key == 'endpoints': - self._endpoints = [] - for endpoint in val: - ep = DSFMonitorEndpoint(**endpoint) - ep._monitor = self - self._endpoints.append(ep) - elif key == 'options': - for opt_key, opt_val in val.items(): - setattr(self, '_' + opt_key, opt_val) - elif key == 'active': - self._active = Active(val) - else: - setattr(self, '_' + key, val) - - @property - def dsf_monitor_id(self): - """The unique system id of this :class:`DSFMonitor`""" - return self._dsf_monitor_id - @dsf_monitor_id.setter - def dsf_monitor_id(self, value): - pass - - @property - def label(self): - """A unique label to identify this :class:`DSFMonitor`""" - return self._label - @label.setter - def label(self, value): - self._label = value - api_args = {'label': self._label} - self._update(api_args) - - @property - def protocol(self): - """The protocol to monitor. Must be one of 'HTTP', 'HTTPS', 'PING', - 'SMTP', or 'TCP' - """ - return self._protocol - @protocol.setter - def protocol(self, value): - self._protocol = value - api_args = {'protocol': self._protocol} - self._update(api_args) - - @property - def response_count(self): - """The number of responses to determine whether or not the endpoint is - 'up' or 'down' - """ - return self._response_count - @response_count.setter - def response_count(self, value): - self._response_count = value - api_args = {'response_count': self._response_count} - self._update(api_args) - - @property - def probe_interval(self): - """How often to run this :class:`DSFMonitor`""" - return self._probe_interval - @probe_interval.setter - def probe_interval(self, value): - self._probe_interval = value - api_args = {'probe_interval': self._probe_interval} - self._update(api_args) - - @property - def retries(self): - """How many retries this :class:`DSFMonitor` should attempt on failure - before giving up. - """ - return self._retries - @retries.setter - def retries(self, value): - self._retries = value - api_args = {'retries': self._retries} - self._update(api_args) + if 'endpoints' in data: + endpoints = data.pop('endpoints') + self._endpoints = [] + for endpoint in endpoints: + ep = DSFMonitorEndpoint(**endpoint) + ep._monitor = self + self._endpoints.append(ep) + if 'options' in data: + options = data.pop('options') + for key, val in options.items(): + data[key] = val + if 'active' in data: + self._active = Active(data.pop('active')) + super(DSFMonitor, self)._build(data) + self.uri = '/DSFMonitor/{0}/'.format(self.dsf_monitor_id) - @property - def active(self): - """Returns whether or not this :class:`DSFMonitor` is active. Will - return either 'Y' or 'N' - """ - return self._active - @active.setter - def active(self, value): - self._active = value - api_args = {'active': self._active} - self._update(api_args) - - @property - def endpoints(self): - """A list of the endpoints (and their statuses) that this - :class:`DSFMonitor` is currently monitoring. - """ - self._get(self.dsf_monitor_id) - return self._endpoints - @endpoints.setter - def endpoints(self, value): - pass - - @property - def options(self): - """Additional options pertaining to this :class:`DSFMonitor`""" - return self._options - @options.setter - def options(self, value): - self._options = value - api_args = {'options': self._options} - self._update(api_args) - - def delete(self): - """Delete an existing :class:`DSFMonitor` from the DynECT System""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + def __str__(self): + return ': {0}'.format(self.dsf_monitor_id) -class TrafficDirector(object): +class TrafficDirector(APIObject): """Traffic Director is a DNS based traffic routing and load balancing service that is Geolocation aware and can support failover by monitoring endpoints. + +from dyn.tm.session import DynectSession +from dyn.tm.services.dsf import TrafficDirector, get_all_dsf_services +Lab_DTE = {'host': 'api2.dte.plab.mht.dyndns.com', 'customer': 'DynDTE', + 'username': 'dte-apitest', 'password': 'dyndns01', 'port': 8002} +s = DynectSession(**Lab_DTE) +# services = get_all_dsf_services() +ethan = TrafficDirector('-inl6Rq8c8KCc2FbgLVEFO4sTGc') """ + uri = '/DSF/' + session_type = DynectSession + _get_length = 1 + service_id = ImmutableAttribute('service_id') + label = StringAttribute('label') + ttl = IntegerAttribute('ttl') + rulesets = ListAttribute('rulesets') + def __init__(self, *args, **kwargs): """Create a :class:`TrafficDirector` object @@ -2041,117 +1472,86 @@ def __init__(self, *args, **kwargs): :param rulesets: A list of :class:`DSFRulesets` that are defined for this :class:`TrafficDirector` service """ - super(TrafficDirector, self).__init__() - self._label = self._ttl = self._publish = self._response_pools = None - self._record_sets = self.uri = self._service_id = None self._notifiers = APIList(DynectSession.get_session, 'notifiers') self._nodes = APIList(DynectSession.get_session, 'nodes') self._rulesets = APIList(DynectSession.get_session, 'rulesets') - if 'api' in kwargs: - del kwargs['api'] - self._build(kwargs) - elif len(args) + len(kwargs) == 1: - self._get(*args, **kwargs) - else: - self._post(*args, **kwargs) - self.uri = '/DSF/{}/'.format(self._service_id) self._notifiers.uri = self._nodes.uri = self._rulesets.uri = self.uri + super(TrafficDirector, self).__init__(*args, **kwargs) def _post(self, label, ttl=None, publish='Y', nodes=None, notifiers=None, rulesets=None): """Create a new :class:`TrafficDirector` on the DynECT System""" uri = '/DSF/' - self._label = label - self._ttl = ttl - self._nodes = nodes - self._notifiers = notifiers - self._rulesets = rulesets - api_args = {'label': self._label, - 'publish': publish} + api_args = {'label': label, 'publish': publish} if ttl: - api_args['ttl'] = self._ttl + api_args['ttl'] = ttl if nodes: - api_args['nodes'] = self._nodes + api_args['nodes'] = nodes if notifiers: if isinstance(notifiers[0], dict): api_args['notifiers'] = notifiers else: # notifiers is a list of Notifier objects - api_args['notifiers'] = [{'notifier_id': x.notifier_id} for x - in self._notifiers] + api_args['notifiers'] = [{'notifier_id': n.notifier_id} for n + in notifiers] if rulesets: - api_args['rulesets'] = [rule._json for rule in self._rulesets] + api_args['rulesets'] = [rule.to_json() for rule in rulesets] response = DynectSession.get_session().execute(uri, 'POST', api_args) - self.uri = '/DSF/{}/'.format(response['data']['service_id']) + self.uri = '/DSF/{0}/'.format(response['data']['service_id']) self._build(response['data']) def _build(self, data): - for key, val in data.items(): - if key == 'notifiers': - # Don't do anything special with these dicts for now - self._notifiers = APIList(DynectSession.get_session, - 'notifiers', None, val) - elif key == 'rulesets': - # Clear Rulesets - self._rulesets = APIList(DynectSession.get_session, 'rulesets') - self._rulesets.uri = None - # For each Ruleset returned, create a new DSFRuleset object - for ruleset in val: - self._rulesets.append(DSFRuleset(**ruleset)) - elif key == 'nodes': - # Don't do anything special with these dicts for now - self._nodes = APIList(DynectSession.get_session, 'nodes', None, - val) - else: - setattr(self, '_' + key, val) - self.uri = '/DSF/{}/'.format(self._service_id) + if 'notifiers' in data: + self._notifiers = APIList(DynectSession.get_session, 'notifiers', + None, data.pop('notifiers')) + if 'rulesets' in data: + self._rulesets = APIList(DynectSession.get_session, 'rulesets') + self._rulesets.uri = None + # For each Ruleset returned, create a new DSFRuleset object + for ruleset in data.pop('rulesets'): + for pool in ruleset.get('response_pools', []): + pool['dsf_id'] = self.service_id + self._rulesets.append(DSFRuleset(**ruleset)) + if 'nodes' in data: + self._nodes = APIList(DynectSession.get_session, 'nodes', None, + data.pop('nodes')) self._notifiers.uri = self._nodes.uri = self._rulesets.uri = self.uri + super(TrafficDirector, self)._build(data) def _get(self, service_id): """Get an existing :class:`TrafficDirector` from the DynECT System""" - self._service_id = service_id - self.uri = '/DSF/{}/'.format(self._service_id) + self.uri = '/DSF/{0}/'.format(service_id) api_args = {'pending_changes': 'Y'} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - self._build(response['data']) + resp = DynectSession.get_session().execute(self.uri, 'GET', api_args) + self._build(resp['data']) - def _update(self, api_args): + def _update(self, **api_args): """Private update method""" if 'publish' not in api_args: api_args['publish'] = 'Y' - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - self._build(response['data']) + super(TrafficDirector, self)._update(**api_args) def revert_changes(self): """Clears the changeset for this service and reverts all non-published changes to their original state """ - api_args = {'revert': True} - self._update(api_args) + self._update(revert=True) def add_notifier(self, notifier_id, publish='Y'): """Links the Notifier with the specified id to this Traffic Director service """ - api_args = {'add_notifier': True, 'notifier_id': notifier_id, - 'publish': publish} - self._update(api_args) + self._update(add_notifier=True, notifier_id=notifier_id, + publish=publish) def remove_orphans(self): """Remove Record Set Chains which are no longer referenced by a :class:`DSFResponsePool` """ - api_args = {'remove_orphans': 'Y'} - self._update(api_args) + self._update(remove_orphans='Y') - @property - def service_id(self): - """The unique System id of this DSF Service""" - return self._service_id - @service_id.setter - def service_id(self, value): - pass + def publish(self): + """Publish any pending changesets for this service""" + self._update(publish='Y') @property def records(self): @@ -2161,9 +1561,6 @@ def records(self): for rs_chains in response_pool.rs_chains for record_set in rs_chains.record_sets for record in record_set.records] - @records.setter - def records(self, value): - pass @property def record_sets(self): @@ -2174,9 +1571,6 @@ def record_sets(self): for response_pool in ruleset.response_pools for rs_chains in response_pool.rs_chains for record_set in rs_chains.record_sets] - @record_sets.setter - def record_sets(self, value): - pass @property def response_pools(self): @@ -2185,9 +1579,6 @@ def response_pools(self): """ return [response_pool for ruleset in self._rulesets for response_pool in ruleset.response_pools] - @response_pools.setter - def response_pools(self, value): - pass @property def rs_chains(self): @@ -2197,86 +1588,6 @@ def rs_chains(self): return [rs_chains for ruleset in self._rulesets for response_pool in ruleset.response_pools for rs_chains in response_pool.rs_chains] - @rs_chains.setter - def rs_chains(self, value): - pass - - @property - def notifiers(self): - """A list of names of notifiers associated with this - :class:`TrafficDirector` service - """ - return self._notifiers - @notifiers.setter - def notifiers(self, value): - if isinstance(value, list) and not isinstance(value, APIList): - self._notifiers = APIList(DynectSession.get_session, 'notifiers', - None, value) - elif isinstance(value, APIList): - self._notifiers = value - self._notifiers.uri = self.uri - - @property - def rulesets(self): - """A list of :class:`DSFRulesets` that are defined for this - :class:`TrafficDirector` service - """ - return self._rulesets - @rulesets.setter - def rulesets(self, value): - if isinstance(value, list) and not isinstance(value, APIList): - self._rulesets = APIList(DynectSession.get_session, 'rulesets', - None, value) - elif isinstance(value, APIList): - self._rulesets = value - self._rulesets.uri = self.uri - - @property - def nodes(self): - """A list of zone, FQDN pairs in a hash that are linked, or to be linked - to this :class:`TrafficDirector` service""" - return self._nodes - @nodes.setter - def nodes(self, value): - if isinstance(value, list) and not isinstance(value, APIList): - self._nodes = APIList(DynectSession.get_session, 'nodes', None, - value) - elif isinstance(value, APIList): - self._nodes = value - self._nodes.uri = self.uri - - @property - def label(self): - """A unique label for this :class:`TrafficDirector` service""" - return self._label - @label.setter - def label(self, value): - self._label = value - api_args = {'label': self._label} - self._update(api_args) - - @property - def ttl(self): - """The default TTL to be used across this service""" - if not isinstance(self._ttl, int): - self._ttl = int(self._ttl) - return self._ttl - @ttl.setter - def ttl(self, value): - self._ttl = value - api_args = {'ttl': self._ttl} - self._update(api_args) - - def delete(self): - """Delete this :class:`TrafficDirector` from the DynECT System""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) def __str__(self): - """str override""" - return force_unicode(': {}').format(self._service_id) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) + return force_unicode(': {0}').format(self.service_id) From 73b6dd08f122de65d24b404f68bb44c33ce18059 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 25 Dec 2014 23:44:35 -0500 Subject: [PATCH 38/59] Bugfix for not forcing _post calls --- dyn/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dyn/core.py b/dyn/core.py index 6be7ff7..1b92eff 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -196,7 +196,7 @@ def __init__(self, *args, **kwargs): if 'api' in kwargs: del kwargs['api'] self._build(kwargs) - elif len(args) == 0 and len(kwargs) == self._get_length: + elif len(args) + len(kwargs) == self._get_length: self._get(*args, **kwargs) else: self._post(*args, **kwargs) From 07b9f1ccefb518c542b78ee70e919dd78cb52b5f Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Thu, 25 Dec 2014 23:53:33 -0500 Subject: [PATCH 39/59] Moving deprecated Geo TM service to dyn_support --- dyn/tm/services/geo.py | 666 ----------------------------------------- 1 file changed, 666 deletions(-) delete mode 100644 dyn/tm/services/geo.py diff --git a/dyn/tm/services/geo.py b/dyn/tm/services/geo.py deleted file mode 100644 index bf4be08..0000000 --- a/dyn/tm/services/geo.py +++ /dev/null @@ -1,666 +0,0 @@ -# -*- coding: utf-8 -*- -"""This module contains API wrappers for the Geo TM service. -NOTE: The Geo Traffic Management Service is deprecated. Legacy users of the -service should contact Concierge for any questions on adding a Geo Traffic -Management service to your zone. All other users should use Traffic Director -(DSF) instead. -""" -import logging - -from ..records import * -from ..session import DynectSession - -__author__ = 'jnappi' -__all__ = ['GeoARecord', 'GeoAAAARecord', 'GeoCERTRecord', 'GeoCNAMERecord', - 'GeoDHCIDRecord', 'GeoDNAMERecord', 'GeoDNSKEYRecord', 'GeoDSRecord', - 'GeoKEYRecord', 'GeoKXRecord', 'GeoLOCRecord', 'GeoIPSECKEYRecord', - 'GeoMXRecord', 'GeoNAPTRRecord', 'GeoPTRRecord', 'GeoPXRecord', - 'GeoNSAPRecord', 'GeoRPRecord', 'GeoNSRecord', 'GeoSPFRecord', - 'GeoSRVRecord', 'GeoTXTRecord', 'GeoRegionGroup', 'Geo'] - - -class GeoARecord(ARecord): - """An :class:`ARecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, weight, serve_count, label=None, ttl=None, *args, - **kwargs): - """Create a :class:`GeoARecord` object - - :param weight: The weight for this :class:`GeoARecord` - :param serve_count: The serve count for this :class:`GeoARecord` - :param *args: Non keyword args for the associated :class:`ARecord` - :param label: A unique label for this :class:`GeoARecord` - :param ttl: TTL for the associated :class:`ARecord`, will override a ttl - specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`ARecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoARecord, self).__init__(*args, **kwargs) - self.weight = weight - self.serve_count = serve_count - self.label = label - self._ttl = ttl - - -class GeoAAAARecord(AAAARecord): - """An :class:`AAAARecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, weight, serve_count, label=None, ttl=None, *args, - **kwargs): - """Create a :class:`GeoAAAARecord` object - - :param weight: The weight for this :class:`GeoAAAARecord` - :param serve_count: The serve count for this :class:`GeoAAAARecord` - :param *args: Non keyword args for the associated :class:`AAAARecord` - :param label: A unique label for this :class:`GeoAAAARecord` - :param ttl: TTL for the associated :class:`AAAARecord` - :param **kwargs: Keyword args for the associated :class:`AAAARecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoAAAARecord, self).__init__(*args, **kwargs) - self.weight = weight - self.serve_count = serve_count - self.label = label - self._ttl = ttl - - -class GeoCERTRecord(CERTRecord): - """An :class:`CERTRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoCERTRecord` object - - :param *args: Non keyword args for the associated :class:`CERTRecord` - :param label: A unique label for this :class:`GeoCERTRecord` - :param ttl: TTL for the associated :class:`CERTRecord` - :param **kwargs: Keyword args for the associated :class:`CERTRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoCERTRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoCNAMERecord(CNAMERecord): - """An :class:`CNAMERecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, weight, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoCNAMERecord` object - - :param weight: The weight for this :class:`GeoCNAMERecord` - :param *args: Non keyword args for the associated :class:`CNAMERecord` - :param label: A unique label for this :class:`GeoCNAMERecord` - :param ttl: TTL for the associated :class:`CNAMERecord` - :param **kwargs: Keyword args for the associated :class:`CNAMERecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoCNAMERecord, self).__init__(*args, **kwargs) - self.weight = weight - self.label = label - self._ttl = ttl - - -class GeoDHCIDRecord(DHCIDRecord): - """An :class:`DHCIDRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoCNAMERecord` object - - :param *args: Non keyword args for the associated :class:`DHCIDRecord` - :param label: A unique label for this :class:`GeoDHCIDRecord` - :param ttl: TTL for the associated :class:`DHCIDRecord` - :param **kwargs: Keyword args for the associated :class:`DHCIDRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoDHCIDRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoDNAMERecord(DNAMERecord): - """An :class:`DNAMERecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoDNAMERecord` object - - :param *args: Non keyword args for the associated :class:`DNAMERecord` - :param label: A unique label for this :class:`GeoDNAMERecord` - :param ttl: TTL for the associated :class:`DNAMERecord` - :param **kwargs: Keyword args for the associated :class:`DNAMERecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoDNAMERecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoDNSKEYRecord(DNSKEYRecord): - """An :class:`DNSKEYRecord` object which is able to store additional data - for use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoDNSKEYRecord` object - - :param *args: Non keyword args for the associated :class:`DNSKEYRecord` - :param label: A unique label for this :class:`GeoDNSKEYRecord` - :param ttl: TTL for the associated :class:`DNSKEYRecord` - :param **kwargs: Keyword args for the associated :class:`DNSKEYRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoDNSKEYRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoDSRecord(DSRecord): - """An :class:`DSRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoDSRecord` object - - :param *args: Non keyword args for the associated :class:`DSRecord` - :param label: A unique label for this :class:`GeoDSRecord` - :param ttl: TTL for the associated :class:`DSRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`DSRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoDSRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoKEYRecord(KEYRecord): - """An :class:`KEYRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoKEYRecord` object - - :param *args: Non keyword args for the associated :class:`KEYRecord` - :param label: A unique label for this :class:`GeoKEYRecord` - :param ttl: TTL for the associated :class:`KEYRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`KEYRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoKEYRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoKXRecord(KXRecord): - """An :class:`KXRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoKXRecord` object - - :param *args: Non keyword args for the associated :class:`KXRecord` - :param label: A unique label for this :class:`GeoKXRecord` - :param ttl: TTL for the associated :class:`KXRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`KXRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoKXRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoLOCRecord(LOCRecord): - """An :class:`LOCRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoLOCRecord` object - - :param *args: Non keyword args for the associated :class:`LOCRecord` - :param label: A unique label for this :class:`GeoLOCRecord` - :param ttl: TTL for the associated :class:`LOCRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`LOCRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoLOCRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoIPSECKEYRecord(IPSECKEYRecord): - """An :class:`IPSECKEYRecord` object which is able to store additional data - for use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoIPSECKEYRecord` object - - :param *args: Non keyword args for the associated - :class:`IPSECKEYRecord` - :param label: A unique label for this :class:`GeoIPSECKEYRecord` - :param ttl: TTL for the associated :class:`IPSECKEYRecord`, will - override a ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`IPSECKEYRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoIPSECKEYRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoMXRecord(MXRecord): - """An :class:`MXRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoMXRecord` object - - :param *args: Non keyword args for the associated :class:`MXRecord` - :param label: A unique label for this :class:`GeoMXRecord` - :param ttl: TTL for the associated :class:`MXRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`MXRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoMXRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoNAPTRRecord(NAPTRRecord): - """An :class:`NAPTRRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoNAPTRRecord` object - - :param *args: Non keyword args for the associated :class:`NAPTRRecord` - :param label: A unique label for this :class:`GeoNAPTRRecord` - :param ttl: TTL for the associated :class:`NAPTRRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`NAPTRRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoNAPTRRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoPTRRecord(PTRRecord): - """An :class:`PTRRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoPTRRecord` object - - :param *args: Non keyword args for the associated :class:`PTRRecord` - :param label: A unique label for this :class:`GeoPTRRecord` - :param ttl: TTL for the associated :class:`PTRRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`PTRRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoPTRRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoPXRecord(PXRecord): - """An :class:`PXRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoPXRecord` object - - :param *args: Non keyword args for the associated :class:`PXRecord` - :param label: A unique label for this :class:`GeoPXRecord` - :param ttl: TTL for the associated :class:`PXRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`PXRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoPXRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoNSAPRecord(NSAPRecord): - """An :class:`NSAPRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoNSAPRecord` object - - :param *args: Non keyword args for the associated :class:`NSAPRecord` - :param label: A unique label for this :class:`GeoNSAPRecord` - :param ttl: TTL for the associated :class:`NSAPRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`NSAPRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoNSAPRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoRPRecord(RPRecord): - """An :class:`RPRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoRPRecord` object - - :param *args: Non keyword args for the associated :class:`RPRecord` - :param label: A unique label for this :class:`GeoRPRecord` - :param ttl: TTL for the associated :class:`RPRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`RPRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoRPRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoNSRecord(NSRecord): - """An :class:`NSRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoNSRecord` object - - :param *args: Non keyword args for the associated :class:`NSRecord` - :param label: A unique label for this :class:`GeoNSRecord` - :param ttl: TTL for the associated :class:`NSRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`NSRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoNSRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoSPFRecord(SPFRecord): - """An :class:`SPFRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoSPFRecord` object - - :param *args: Non keyword args for the associated :class:`SPFRecord` - :param label: A unique label for this :class:`GeoSPFRecord` - :param ttl: TTL for the associated :class:`SPFRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`SPFRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoSPFRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoSRVRecord(SRVRecord): - """An :class:`SRVRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoSRVRecord` object - - :param *args: Non keyword args for the associated :class:`SRVRecord` - :param label: A unique label for this :class:`GeoSRVRecord` - :param ttl: TTL for the associated :class:`SRVRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`SRVRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoSRVRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoTXTRecord(TXTRecord): - """An :class:`TXTRecord` object which is able to store additional data for - use by a :class:`Geo` service. - """ - def __init__(self, label=None, ttl=None, *args, **kwargs): - """Create a :class:`GeoTXTRecord` object - - :param *args: Non keyword args for the associated :class:`TXTRecord` - :param label: A unique label for this :class:`GeoTXTRecord` - :param ttl: TTL for the associated :class:`TXTRecord`, will override a - ttl specified in *args or **kwargs - :param **kwargs: Keyword args for the associated :class:`TXTRecord` - """ - # Set api flag so the record isn't really created - kwargs['create'] = False - super(GeoTXTRecord, self).__init__(*args, **kwargs) - self.label = label - self._ttl = ttl - - -class GeoRegionGroup(object): - """docstring for GeoRegionGroup""" - def __init__(self, countries, name, geo_records, **kwargs): - """Create a :class:`GeoRegionGroup` object - - :param countries: A list of ISO-3166 two letter codes to represent the - names of countries and their subdivisions or one of the predefined - groups. - :param name: Name of this :class:`GeoRegionGroup` - :param geo_records: A `list` of custom Geo DNSRecord subclass type - objects - """ - super(GeoRegionGroup, self).__init__() - self.uri = None - self._service_name = self._group_name = self._countries = None - self._countries = countries - self._name = name - self.geo_records = geo_records - # TODO: implement return data (GET) object creation - if 'api' in kwargs: - del kwargs['api'] - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - - def _post(self, countries, name, geo_records): - self._countries = countries - self._name = name - self.geo_records = geo_records - api_args = {'service_name': self._service_name, - 'group_name': self._group_name, - 'countries': self._countries, 'name': self._name} - return api_args - - def _get(self): - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - @property - def countries(self): - return self._countries - - def delete(self): - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) - - -class Geo(object): - """docstring for Geo""" - def __init__(self, service_name, *args, **kwargs): - """Create a new :class:`Geo` object - - :param service_name: The name of this Geo Service - :param groups: A list of :class:`GeoRegionGroup`'s - :param nodes: A list of :class:`GeoNode`'s - :param ttl: Time to Live for each record in the service - """ - super(Geo, self).__init__() - self._service_name = service_name - self.uri = '/Geo/{}/'.format(self._service_name) - self._groups = self._nodes = self._ttl = None - if 'api' in kwargs: - del kwargs['api'] - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - elif len(args) == 0 and len(kwargs) == 0: - self._get() - else: - self._post(*args, **kwargs) - - def _post(self, groups, ttl=None): - """Create a new :class:`Geo` service on the DynECT System""" - self._groups = groups - self._ttl = ttl - api_args = {'groups': []} - nodes = [] - for group in groups: - weight = {} - serve_count = {} - ttls = {} - label = {} - rdata = {} - for record in group.geo_records: - weight_name = ''.join([record.rec_name, '_weight']) - serve_name = ''.join([record.rec_name, '_serve_count']) - ttl_name = ''.join([record.rec_name, '_ttl']) - label_name = ''.join([record.rec_name, '_label']) - rdata_name = ''.join([record.rec_name, '_rdata']) - # Build weight hash - if hasattr(record, 'weight'): - if weight_name in weight: - weight[weight_name].append(record.weight) - else: - weight[weight_name] = [record.weight] - # Build serve_count hash - if hasattr(record, 'serve_count'): - serve_count[serve_name] = str(record.serve_count) - # Build ttl hash - if ttl_name in serve_count: - ttls[ttl_name] = str(record.ttl) or str(self._ttl) - # Build label hash - autolabel = '' - if label_name in label: - label[label_name].append(record.label or autolabel) - else: - label[label_name] = [record.label or autolabel] - # Build rdata hash - if rdata_name in rdata: - rdata[rdata_name].append(record.geo_rdata) - else: - rdata[rdata_name] = [record.geo_rdata] - nodes.append(record.geo_node) - group_data = {'weight': weight, 'serve_count': serve_count, - 'ttl': ttls, 'label': label, 'rdata': rdata, - 'countries': group.countries, 'name': group._name} - api_args['groups'].append(group_data) - if self._ttl: - api_args['ttl'] = self._ttl - api_args['nodes'] = nodes - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) - for key, val in response['data'].items(): - if key == 'groups': - pass - elif key == 'nodes': - pass - else: - setattr(self, '_' + key, val) - - def _get(self): - """Get an existing :class:`Geo` service from the DynECT System""" - api_args = {} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - for key, val in response['data'].items(): - if key == 'groups': - pass - elif key == 'nodes': - pass - else: - setattr(self, '_' + key, val) - - def _update(self, api_args): - """Private Update method""" - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - if key == 'groups': - pass - elif key == 'nodes': - pass - else: - setattr(self, '_' + key, val) - - @property - def service_name(self): - return self._service_name - @service_name.setter - def service_name(self, new_name): - self._service_name = new_name - api_args = {'new_name': self._service_name} - self._update(api_args) - self.uri = '/Geo/{}/'.format(self._service_name) - - @property - def groups(self): - return self._groups - @groups.setter - def groups(self, value): - self._groups = value - api_args = {'groups': self._groups.to_json()} - self._update(api_args) - - @property - def nodes(self): - return self._nodes - @nodes.setter - def nodes(self, value): - self._nodes = value - api_args = {'nodes': self._nodes.to_json()} - self._update(api_args) - - def activate(self): - """Activate this :class:`Geo` service on the DynECT System""" - api_args = {'activate': True} - self._update(api_args) - - def deactivate(self): - """Deactivate this :class:`Geo` service on the DynECT System""" - api_args = {'deactivate': True} - self._update(api_args) - - def delete(self): - """Delete this :class:`Geo` service from the DynECT System""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) From 47e19b6bd9909c1c7fc7e96f225ec97a810052f4 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Fri, 26 Dec 2014 00:06:42 -0500 Subject: [PATCH 40/59] Updating TLSARecord to match 2.0.0 design patterns --- dyn/tm/records.py | 131 ++++++++-------------------------------------- 1 file changed, 21 insertions(+), 110 deletions(-) diff --git a/dyn/tm/records.py b/dyn/tm/records.py index c871546..f79c11e 100644 --- a/dyn/tm/records.py +++ b/dyn/tm/records.py @@ -527,123 +527,34 @@ def rdata(self): class TLSARecord(DNSRecord): - """The TLSA record is used to associate a TLS server - certificate or public key with the domain name where the record is - found, thus forming a "TLSA certificate association". Defined in RFC 6698 + """The TLSA record is used to associate a TLS server certificate or public + key with the domain name where the record is found, thus forming a + "TLSA certificate association". Defined in RFC 6698 """ + record_type = 'TLSARecord' - def __init__(self, zone, fqdn, *args, **kwargs): - """Create an :class:`~dyn.tm.records.TLSARecord` object - - :param zone: Name of zone where the record will be added - :param fqdn: Name of node where the record will be added - :param cert_usage: Specifies the provided association that will be used - to match the certificate presented in the TLS handshake. Example - values: 0 (CA constraint), 1 (Service certificate constraint), - 2 (Trust anchor assertion ), 3 (Domain-issued certificate) - :param selector: Specifies which part of the TLS certificate presented - by the server will be matched against the association data. Example - values: 0 (Full certificate), 1 (SubjectPublicKeyInfo) - :param match_type: Specifies how the certificate association is - presented. Example values: 0 (No hash used), 1 (SHA-256), - 2 (SHA-512) - :param certificate: Full certificate or its SubjectPublicKeyInfo, or - hash based on the matching type. - :param ttl: TTL for the record in seconds - """ - if 'create' in kwargs: - super(TLSARecord, self).__init__(zone, fqdn, kwargs['create']) - del kwargs['create'] - self._build(kwargs) - self._record_type = 'TLSARecord' - else: - super(TLSARecord, self).__init__(zone, fqdn) - self._record_type = 'TLSARecord' - self._cert_usage = self._selector = None - self._mathc_type = self._certificate = None - if 'record_id' in kwargs: - self._get_record(kwargs['record_id']) - elif len(args) + len(kwargs) == 1: - self._get_record(*args, **kwargs) - elif 'cert_usage' in kwargs or 'selector' in \ - kwargs or 'match_type' in kwargs or \ - 'certificate' in kwargs or 'ttl' in kwargs: - self._post(*args, **kwargs) - elif len(args) + len(kwargs) >= 1: - self._post(*args, **kwargs) - - def _post(self, cert_usage, selector, match_type, certificate, ttl=0): - """Create a new :class:`~dyn.tm.records.TLSARecord` on the DynECT System - """ - self._ttl = ttl - self._cert_usage = cert_usage - self._selector = selector - self._match_type = match_type - self._certificate = certificate - self.api_args = {'rdata': {'cert_usage': self._cert_usage, - 'selector': self._selector, - 'match_type': self._match_type, - 'certificate': self._certificate}, - 'ttl': self._ttl} - self._create_record(self.api_args) + #: Specifies the provided association that will be used to match the + #: certificate presented in the TLS handshake + cert_usage = IntegerAttribute('cert_usage') + + #: Specifies which part of the TLS certificate presented by the server will + #: be matched against the association data. + selector = IntegerAttribute('selector') + + #: Specifies how the certificate association is presented. + match_type = IntegerAttribute('match_type') + + #: Full certificate or its SubjectPublicKeyInfo, or hash based on the + #: matching type + certificate = StringAttribute('certificate') def rdata(self): - """Return this :class:`~dyn.tm.records.TLSARecord`'s rdata as a JSON blob + """Return this :class:`~dyn.tm.records.TLSARecord`'s rdata as a JSON + blob """ guts = super(TLSARecord, self).rdata() shell = {'tlsa_rdata': guts} return shell - - @property - def cert_usage(self): - """Specifies the provided association that will be used - to match the certificate presented in the TLS handshake - """ - return self._cert_usage - - @cert_usage.setter - def cert_usage(self, value): - self._cert_usage = value - self.api_args['rdata']['cert_usage'] = self._cert_usage - self._update_record(self.api_args) - - @property - def selector(self): - """Specifies which part of the TLS certificate presented - by the server will be matched against the association data. - """ - return self._selector - - @selector.setter - def selector(self, value): - self._selector = value - self.api_args['rdata']['selector'] = self._selector - self._update_record(self.api_args) - - @property - def match_type(self): - """Specifies how the certificate association is presented. - """ - return self._match_type - - @match_type.setter - def match_type(self, value): - self._match_type = value - self.api_args['rdata']['match_type'] = self._match_type - self._update_record(self.api_args) - - @property - def certificate(self): - """Full certificate or its SubjectPublicKeyInfo, or - hash based on the matching type - """ - return self._certificate - - @certificate.setter - def certificate(self, value): - self._certificate = value - self.api_args['rdata']['certificate'] = self._certificate - self._update_record(self.api_args) class TXTRecord(DNSRecord): @@ -669,5 +580,5 @@ def rdata(self): 'IPSECKEY': IPSECKEYRecord, 'MX': MXRecord, 'NAPTR': NAPTRRecord, 'PTR': PTRRecord, 'PX': PXRecord, 'NSAP': NSAPRecord, 'RP': RPRecord, 'NS': NSRecord, 'SOA': SOARecord, 'SPF': SPFRecord, 'SRV': SRVRecord, - 'TXT': TXTRecord + 'TLSA': TLSARecord,'TXT': TXTRecord } From aa33b97c09ecde8f5824f9f1be9b7e5072abe626 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Fri, 26 Dec 2014 16:23:54 -0500 Subject: [PATCH 41/59] Adding default parameter to all APIDescriptor objects --- dyn/core.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/dyn/core.py b/dyn/core.py index 1b92eff..9790688 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -67,14 +67,15 @@ class APIDescriptor(object): API responses """ - def __init__(self, name=''): + def __init__(self, name='', default=None): super(APIDescriptor, self).__init__() self.name = name self.private_name = '_' + self.name + self.default = default def __get__(self, instance, cls): """Return a handle on the private instance of this descriptor""" - return getattr(instance, self.private_name, None) + return getattr(instance, self.private_name, self.default) def __set__(self, instance, value): """Update the value of the private attribute represented by this @@ -129,9 +130,9 @@ class ClassAttribute(TypedAttribute): for API attributes that are wrapped in something else to make them more easy to use """ - def __init__(self, name='', class_type=object): + def __init__(self, name='', default=None, class_type=object): self.ty = class_type - super(ClassAttribute, self).__init__(name) + super(ClassAttribute, self).__init__(name=name, default=default) class ValidatedAttribute(APIDescriptor): @@ -139,13 +140,13 @@ class ValidatedAttribute(APIDescriptor): values """ - def __init__(self, name='', validator=None): + def __init__(self, name='', default=None, validator=None): """An API field that must be one of a specific set of values :param name: The name of this field :param validator: An optional list of valid values for this field """ - super(ValidatedAttribute, self).__init__(name) + super(ValidatedAttribute, self).__init__(name=name, default=default) self.validator = validator def __set__(self, instance, value): From 04a989557405a1ab59b290e10c382ba8f9c89db0 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Fri, 26 Dec 2014 16:24:16 -0500 Subject: [PATCH 42/59] Documented and alphabetized all record types --- dyn/tm/records.py | 273 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 215 insertions(+), 58 deletions(-) diff --git a/dyn/tm/records.py b/dyn/tm/records.py index f79c11e..a3f2974 100644 --- a/dyn/tm/records.py +++ b/dyn/tm/records.py @@ -24,10 +24,22 @@ class DNSRecord(APIObject): record type objects """ session_type = DynectSession + + #: Name of zone that this record belongs to zone = ImmutableAttribute('zone') + + #: The fully qualified domain name where this record can be found fqdn = ImmutableAttribute('fqdn') + + #: The unique DynECT System id for this record. Note, that this id will + #: change when a record is updated. Outdated record id's can still be used + #: to retrieve older versions of records from the DynECT system record_id = ImmutableAttribute('record_id') + + #: The time to live for this DNS Record ttl = IntegerAttribute('ttl') + + #: A :const:`str` representation of what type this record is record_type = '' def __init__(self, zone, fqdn, record_id=None, *args, **kwargs): @@ -113,6 +125,8 @@ class ARecord(DNSRecord): """The IPv4 Address (A) Record forward maps a host name to an IPv4 address. """ record_type = 'ARecord' + + #: IPv4 address for the record address = StringAttribute('address') def rdata(self): @@ -133,6 +147,8 @@ class AAAARecord(DNSRecord): addresses and is the current IETF recommendation for this purpose. """ record_type = 'AAAARecord' + + #: IPv6 address for the record address = StringAttribute('address') def rdata(self): @@ -154,9 +170,17 @@ class CERTRecord(DNSRecord): certificates or Certificate Revocation Lists (CRL) in the zone file. """ record_type = 'CERTRecord' + + #: Numeric value for the certificate type format = IntegerAttribute('format') + + #: Numeric value for the public key certificate tag = IntegerAttribute('tag') + + #: Public key algorithm number used to generate the certificate algorithm = StringAttribute('algorithm') + + #: The public key certificate certificate = StringAttribute('certificate') def rdata(self): @@ -173,6 +197,8 @@ class CNAMERecord(DNSRecord): name that may lie inside or outside the current zone. """ record_type = 'CNAMERecord' + + #: The hostname that this CNAME Record points to cname = StringAttribute('cname') def rdata(self): @@ -191,6 +217,8 @@ class DHCIDRecord(DNSRecord): dynamic DNS updates to the same zone. """ record_type = 'DHCIDRecord' + + #: Base-64 encoded digest of DHCP data digest = StringAttribute('digest') def rdata(self): @@ -210,6 +238,8 @@ class DNAMERecord(DNSRecord): a DNAME is equivalent to a CNAME when used in a reverse-map zone file. """ record_type = 'DNAMERecord' + + #: Target hostname pointed to by this :class:`~dyn.tm.records.DNAMERecord` dname = StringAttribute('dname') def rdata(self): @@ -227,9 +257,19 @@ class DNSKEYRecord(DNSRecord): authenticate signed keys or zones. """ record_type = 'DNSKEYRecord' + + #: Numeric value for protocol protocol = IntegerAttribute('protocol') + + #: The public key for the DNSSEC signed zone public_key = StringAttribute('public_key') + + #: Numeric value representing the public key encryption algorithm which + #: will sign the zone. Must be one of 1 (RSA-MD5), 2 (Diffie-Hellman), + #: 3 (DSA/SHA-1), 4 (Elliptic Curve), or 5 (RSA-SHA-1) algorithm = ValidatedAttribute('algorithm', validator=range(1, 6)) + + #: Numeric value confirming this is the zone's DNSKEY flags = IntegerAttribute('flags') def rdata(self): @@ -247,9 +287,21 @@ class DSRecord(DNSRecord): zone. """ record_type = 'DSRecord' + + #: The digest in hexadecimal form. 20-byte, hexadecimal-encoded, one-way + #: hash of the DNSKEY record surrounded by parenthesis characters '(' & ')' digest = StringAttribute('digest') + + #: The digest mechanism to use to verify the digest keytag = IntegerAttribute('keytag') + + #: Numeric value representing the public key encryption algorithm which + #: will sign the zone. Must be one of 1 (RSA-MD5), 2 (Diffie-Hellman), + #: 3 (DSA/SHA-1), 4 (Elliptic Curve), or 5 (RSA-SHA-1) algorithm = ValidatedAttribute('algorithm', validator=range(1, 6)) + + #: the digest mechanism to use to verify the digest. Valid values are + #: 'SHA1' or 'SHA256' digtype = ValidatedAttribute('digtype', validator=('SHA1', 'SHA256')) def rdata(self): @@ -260,6 +312,38 @@ def rdata(self): return shell +class IPSECKEYRecord(DNSRecord): + """The IPSECKEY is used for storage of keys used specifically for IPSec + oerations + """ + record_type = 'IPSECKEYRecord' + + #: Indicates priority among multiple IPSECKEYS. Lower numbers are higher + #: priority + precedence = IntegerAttribute('precedence') + + #: Gateway type. Must be one of 0, 1, 2, or 3 + gatetype = ValidatedAttribute('gatetype', validator=range(0, 4)) + + #: Public key's cryptographic algorithm and format. Must be one of 0, 1, or + #: 2 + algorithm = ValidatedAttribute('algorithm', validator=range(0, 3)) + + #: Gateway used to create IPsec tunnel. Based on Gateway type + gateway = IntegerAttribute('gateway') + + #: Base64 encoding of the public key. Whitespace is allowed + public_key = StringAttribute('public_key') + + def rdata(self): + """Return this :class:`~dyn.tm.records.IPSECKEYRecord`'s rdata as a + JSON blob + """ + guts = super(IPSECKEYRecord, self).rdata() + shell = {'ipseckey_rdata': guts} + return shell + + class KEYRecord(DNSRecord): """"Public Key" (KEY) Records are used for the storage of public keys for use by multiple applications such as IPSec, SSH, etc..., as well as for use @@ -269,9 +353,19 @@ class KEYRecord(DNSRecord): due to the difficulty of querying for specific uses. """ record_type = 'KEYRecord' + + #: Numeric value representing the public key encryption algorithm which + #: will sign the zone. Must be one of 1 (RSA-MD5), 2 (Diffie-Hellman), + #: 3 (DSA/SHA-1), 4 (Elliptic Curve), or 5 (RSA-SHA-1) algorithm = ValidatedAttribute('algorithm', validator=range(1, 6)) + + #: See RFC 2535 for information on valid KEY record flags flags = IntegerAttribute('flags') + + #: Numeric identifier of the protocol for this KEY record protocol = IntegerAttribute('protocol') + + #: The public key for this record public_key = StringAttribute('public_key') def rdata(self): @@ -288,7 +382,15 @@ class KXRecord(DNSRecord): alternative hosts. """ record_type = 'KXRecord' + + #: Hostname that will act as the Key Exchanger. The hostname must have a + #: :class:`~dyn.tm.records.CNAMERecord`, an + #: :class:`~dyn.tm.records.ARecord` and/or an + #: :class:`~dyn.tm.records.AAAARecord` associated with it exchange = StringAttribute('exchange') + + #: Numeric value for priority usage. Lower value takes precedence over + #: higher value where two records of the same type exist on the zone/node preference = IntegerAttribute('preference') def rdata(self): @@ -304,12 +406,21 @@ class LOCRecord(DNSRecord): geographic positioning information associated with a host or service name. """ record_type = 'LOCRecord' + + #: Measured in meters above sea level altitude = IntegerAttribute('altitude') - horiz_pre = StringAttribute('horiz_pre') + + #: Measured in degrees, minutes, and seconds with N/S indicator for North + #: and South latitude = IntegerAttribute('latitude') + + #: Measured in degrees, minutes, and seconds with E/W indicator for East + #: and West longitude = IntegerAttribute('longitude') + + horiz_pre = StringAttribute('horiz_pre') size = IntegerAttribute('size') - version = ValidatedAttribute('version', validator=(0,)) + version = ValidatedAttribute('version', validator=(0,), default=0) vert_pre = StringAttribute('vert_pre') def rdata(self): @@ -321,32 +432,18 @@ def rdata(self): return shell -class IPSECKEYRecord(DNSRecord): - """The IPSECKEY is used for storage of keys used specifically for IPSec - oerations - """ - record_type = 'IPSECKEYRecord' - precedence = IntegerAttribute('precedence') - gatetype = ValidatedAttribute('gatetype', validator=range(0, 4)) - algorithm = ValidatedAttribute('algorithm', validator=range(0, 3)) - gateway = IntegerAttribute('gateway') - public_key = StringAttribute('public_key') - - def rdata(self): - """Return this :class:`~dyn.tm.records.IPSECKEYRecord`'s rdata as a - JSON blob - """ - guts = super(IPSECKEYRecord, self).rdata() - shell = {'ipseckey_rdata': guts} - return shell - - class MXRecord(DNSRecord): """The "Mail Exchanger" record type specifies the name and relative preference of mail servers for a Zone. Defined in RFC 1035 """ record_type = 'MXRecord' + + #: Hostname of the server responsible for accepting mail messages in the + #: zone exchange = StringAttribute('exchange') + + #: Numeric value for priority usage. Lower value takes precedence over + #: higher value where two records of the same type exist on the zone/node. preference = IntegerAttribute('preference') def rdata(self): @@ -363,12 +460,30 @@ class NAPTRRecord(DNSRecord): `rule` that may be applied to private data owned by a client application. """ record_type = 'NAPTRRecord' + + #: Indicates the required priority for processing NAPTR records. Lowest + #: value is used first. order = IntegerAttribute('order') + + #: Indicates priority where two or more NAPTR records have identical order + #: values. Lowest value is used first. preference = IntegerAttribute('preference') + + #: Always starts with 'e2u+' (E.164 to URI). After the 'e2u+' there is a + #: string that defines the type and optionally the subtype of the URI where + #: this :class:`~dyn.tm.records.NAPTRRecord` points. services = StringAttribute('services') + + #: The NAPTR record accepts regular expressions regexp = StringAttribute('regexp') + + #: The next domain name to find. Only applies if this + #: :class:`~dyn.tm.records.NAPTRRecord` is non-terminal. replacement = StringAttribute('replacement') - flags = StringAttribute('flags') + + #: Should be the letter 'U'. This indicates that this is a terminal NAPTR + #: record + flags = StringAttribute('flags', default='U') def rdata(self): """Return this :class:`~dyn.tm.records.NAPTRRecord`'s rdata as a JSON @@ -379,11 +494,52 @@ def rdata(self): return shell +class NSAPRecord(DNSRecord): + """The Network Services Access Point record is the equivalent of an RR for + ISO's Open Systems Interconnect (OSI) system in that it maps a host name to + an endpoint address. + """ + record_type = 'NSAPRecord' + + #: Hex-encoded NSAP identifier + nsap = StringAttribute('nsap') + + def rdata(self): + """Return this :class:`~dyn.tm.records.NSAPRecord`'s rdata as a JSON + blob + """ + guts = super(NSAPRecord, self).rdata() + shell = {'nsap_rdata': guts} + return shell + + +class NSRecord(DNSRecord): + """Nameserver Records are used to list all the nameservers that will + respond authoritatively for a domain. + """ + record_type = 'NSRecord' + + #: Hostname of the authoritative Nameserver for the zone + nsdname = StringAttribute('nsdname') + + #: Hostname of the authoritative Nameserver for the zone + service_class = StringAttribute('service_class') + + def rdata(self): + """Return this :class:`~dyn.tm.records.NSRecord`'s rdata as a JSON blob + """ + guts = super(NSRecord, self).rdata() + shell = {'ns_rdata': guts} + return shell + + class PTRRecord(DNSRecord): """Pointer Records are used to reverse map an IPv4 or IPv6 IP address to a host name """ record_type = 'PTRRecord' + + #: The hostname where the IP address should be directed ptrdname = StringAttribute('ptrdname') def rdata(self): @@ -401,8 +557,15 @@ class PXRecord(DNSRecord): gateway. """ record_type = 'PXRecord' + + #: Sets priority for processing records of the same type. Lowest value is + #: processed first. preference = StringAttribute('preference') + + #: mail hostname map822 = StringAttribute('map822') + + #: The domain name derived from the X.400 part of MCGAM mapx400 = StringAttribute('mapx400') def rdata(self): @@ -414,23 +577,6 @@ def rdata(self): return shell -class NSAPRecord(DNSRecord): - """The Network Services Access Point record is the equivalent of an RR for - ISO's Open Systems Interconnect (OSI) system in that it maps a host name to - an endpoint address. - """ - record_type = 'NSAPRecord' - nsap = StringAttribute('nsap') - - def rdata(self): - """Return this :class:`~dyn.tm.records.NSAPRecord`'s rdata as a JSON - blob - """ - guts = super(NSAPRecord, self).rdata() - shell = {'nsap_rdata': guts} - return shell - - class RPRecord(DNSRecord): """The Respnosible Person record allows an email address and some optional human readable text to be associated with a host. Due to privacy and spam @@ -439,7 +585,12 @@ class RPRecord(DNSRecord): and debugging network problems. """ record_type = 'RPRecord' + + #: Email address of the Responsible Person. mbox = StringAttribute('mbox') + + #: Hostname where a TXT record exists with more information on the + #: responsible person. txtdname = StringAttribute('txtdname') def rdata(self): @@ -450,22 +601,6 @@ def rdata(self): return shell -class NSRecord(DNSRecord): - """Nameserver Records are used to list all the nameservers that will - respond authoritatively for a domain. - """ - record_type = 'NSRecord' - nsdname = StringAttribute('nsdname') - service_class = StringAttribute('service_class') - - def rdata(self): - """Return this :class:`~dyn.tm.records.NSRecord`'s rdata as a JSON blob - """ - guts = super(NSRecord, self).rdata() - shell = {'ns_rdata': guts} - return shell - - class SOARecord(DNSRecord): """The Start of Authority Record describes the global properties for the Zone (or domain). Only one SOA Record is allowed under a zone at any given @@ -473,8 +608,15 @@ class SOARecord(DNSRecord): delete SOA records on the Dynect System. """ record_type = 'SOARecord' + + #: Domain name which specifies the mailbox of the person responsible for + #: this zone rname = StringAttribute('rname') + + #: The style of the zone's serial serial_style = StringAttribute('serial_style') + + #: The minimum TTL for this :class:`~dyn.tm.records.SOARecord`'s zone minimum = IntegerAttribute('minimum') def rdata(self): @@ -496,6 +638,8 @@ class SPFRecord(DNSRecord): sender is authorized to send main for the sender's domain. """ record_type = 'SPFRecord' + + #: Free text containing SPF record information txtdata = StringAttribute('txtdata') def rdata(self): @@ -513,10 +657,21 @@ class SRVRecord(DNSRecord): located can interrogate for the relevant SRV that describes the service. """ record_type = 'SRVRecord' - txtdata = StringAttribute('txtdata') + + #: Indicates the port where the service is running port = IntegerAttribute('port') + + #: Numeric value for priority usage. Lower value takes precedence over + #: higher value where two records of the same type exist on the zone/node priority = IntegerAttribute('priority') + + #: The domain name of a host where the service is running on the specified + #: port target = StringAttribute('target') + + #: Secondary prioritizing of records to serve. Records of equal priority + #: should be served based on their weight. Higher values are served more + #: often weight = IntegerAttribute('weight') def rdata(self): @@ -563,6 +718,8 @@ class TXTRecord(DNSRecord): host, service contacts, or any other required system information. """ record_type = 'TXTRecord' + + #: Free form text txtdata = StringAttribute('txtdata') def rdata(self): @@ -580,5 +737,5 @@ def rdata(self): '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 + 'TLSA': TLSARecord, 'TXT': TXTRecord } From 208e77ff9da51d93804cdc476a5d4d15ee033ad7 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Mon, 29 Dec 2014 13:00:03 -0500 Subject: [PATCH 43/59] Updating DSF docs. --- dyn/tm/services/dsf.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/dyn/tm/services/dsf.py b/dyn/tm/services/dsf.py index 5e470cc..d19fdc9 100644 --- a/dyn/tm/services/dsf.py +++ b/dyn/tm/services/dsf.py @@ -1047,7 +1047,8 @@ def _post(self, dsf_id, dsf_response_pool_id): self._build(resp['data']) def _get(self, dsf_id, dsf_response_pool_id): - """Retrieve an existing :class:`DSFFailoverChain` from the Dynect System + """Retrieve an existing :class:`DSFFailoverChain` from the Dynect + System :param dsf_id: The unique system id of the DSF service this :class:`DSFFailoverChain` is attached to @@ -1442,21 +1443,21 @@ class TrafficDirector(APIObject): """Traffic Director is a DNS based traffic routing and load balancing service that is Geolocation aware and can support failover by monitoring endpoints. - -from dyn.tm.session import DynectSession -from dyn.tm.services.dsf import TrafficDirector, get_all_dsf_services -Lab_DTE = {'host': 'api2.dte.plab.mht.dyndns.com', 'customer': 'DynDTE', - 'username': 'dte-apitest', 'password': 'dyndns01', 'port': 8002} -s = DynectSession(**Lab_DTE) -# services = get_all_dsf_services() -ethan = TrafficDirector('-inl6Rq8c8KCc2FbgLVEFO4sTGc') """ uri = '/DSF/' session_type = DynectSession _get_length = 1 + + #: The unique DynECT system id for this Traffic Director service service_id = ImmutableAttribute('service_id') + + #: A unique label describing this Traffic Director service label = StringAttribute('label') + + #: The default TTL to be used across this service ttl = IntegerAttribute('ttl') + + #: A list of :class:`DSFRulesets` that are defined for this service rulesets = ListAttribute('rulesets') def __init__(self, *args, **kwargs): From ed1641b82d27d19642185ff84f2f2b13cb20d9f4 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Mon, 29 Dec 2014 13:17:13 -0500 Subject: [PATCH 44/59] Completed documention class attribuets of DSF classes --- dyn/tm/services/dsf.py | 95 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/dyn/tm/services/dsf.py b/dyn/tm/services/dsf.py index d19fdc9..fd95c74 100644 --- a/dyn/tm/services/dsf.py +++ b/dyn/tm/services/dsf.py @@ -838,15 +838,37 @@ class DSFRecordSet(APIObject): session_type = DynectSession records = ListAttribute('records') status = ImmutableAttribute('status') + + #: A unique label for this DSF Record Set label = StringAttribute('label') + + #: The type of rdata represented by this DFS Record Set rdata_class = StringAttribute('rdata_class') + + #: Default TTL for all DSF Record's within this DSF Record SEt ttl = IntegerAttribute('ttl') + + #: Defines how eligibility can be changed in response to monitoring. Must + #: be one of 'auto', 'auto_down', or 'manual' automation = ValidatedAttribute('automation', validator=('auto', 'auto_down', 'manual')) + + #: How many records to serve out of this DSF Record Set serve_count = IntegerAttribute('serve_count') + + #: The number of records that must be failing before this DSF Record Set + #: becomes ineligible fail_count = IntegerAttribute('fail_count') + + #: The number of Records that must not be okay before this DSFRecordSet + #: becomes in trouble. trouble_count = IntegerAttribute('trouble_count') + + #: Indicates whether or not this DSFRecordSet can be served. eligible = BooleanAttribute('eligible') + + #: The unique DynECT system id of the DSF Monitor attached to this + #: DSFRecordSet dsf_monitor_id = ImmutableAttribute('dsf_monitor_id') def __init__(self, rdata_class, label=None, ttl=None, automation=None, @@ -994,10 +1016,23 @@ def __str__(self): class DSFFailoverChain(APIObject): uri = '/DSFRecordSetFailoverChain/{0}/{1}/' session_type = DynectSession + + #: A unique label for this DSF Failover Chain label = StringAttribute('label') + + #: Indicates whether or not the contained DSF Record Sets are core record + #: sets core = BooleanAttribute('core') + + #: A :const:`list` of :class:`DSFRecordSet`'s for this DSF Failover Chain record_sets = ListAttribute('record_sets') + + #: The unique DynECT system id for the response pool that this DSF Failover + #: Chain belongs to dsf_response_pool_id = ImmutableAttribute('dsf_response_pool_id') + + #: The unique system id for the Traffic Director service that this failover + #: chain belongs to dsf_id = ImmutableAttribute('dsf_id') def __init__(self, label=None, core=None, record_sets=None, **kwargs): @@ -1090,14 +1125,37 @@ class DSFResponsePool(APIObject): uri = '/DSFResponsePool/{0}/{1}/' session_type = DynectSession _get_length = 1 + + #: A unique label for this DSF Response Pool label = StringAttribute('label') + + #: If fewer than this number of core record sets are eligible, status will + #: be set to 'fail' core_set_count = IntegerAttribute('core_set_count') + + #: Indicates whether or not this DSF Response Pool can be served eligible = BooleanAttribute('eligible') + + #: Defines how eligibility can be changed in response to monitoring automation = StringAttribute('automation') + + #: The unique DynECT system id for the :class:`DSFRuleset` that this + #: response pool belongs to dsf_ruleset_id = ImmutableAttribute('dsf_ruleset_id') + + #: The unique DynECT system id for this DSF Response Pool dsf_response_pool_id = ImmutableAttribute('dsf_response_pool_id') + + #: When specified with `dsf_ruleset_id`, indicates the position of the + #: DSF Response Pool index = IntegerAttribute('index') + + #: A :const:`list` of :class:`DSFFailoverChain`'s that are defined for this + #: DSF Response Pool rs_chains = ImmutableAttribute('rs_chains') + + #: The unique DynECT system id for the Traffic Director service that this + #: DFS Response Pool belongs to dsf_id = ImmutableAttribute('dsf_id') def __init__(self, label, dsf_id, core_set_count=1, eligible=True, @@ -1189,10 +1247,19 @@ def __str__(self): class DSFRuleset(APIObject): session_type = DynectSession + + #: A unique label for this DSF Ruleset label = StringAttribute('label') + + #: A set of rules describing what traffic is applied to this DSF Ruleset. + #: Must be one of 'always' or 'geoip' criteria_type = ValidatedAttribute('criteria_type', validator=('always', 'geoip')) + + #: Varied depending on this specified `criteria_type` criteria = ListAttribute('criteria') + + #: A :const:`list` of :class:`DSFResponsePool`'s for this DSF Ruleset response_pools = ImmutableAttribute('response_pools') def __init__(self, label, criteria_type, response_pools, criteria=None, @@ -1365,19 +1432,41 @@ def site_prefs(self, value): class DSFMonitor(APIObject): """A Monitor for a :class:`TrafficDirector` Service""" - uri = '' + uri = '/DSFMonitor/' session_type = DynectSession _get_length = 1 + + #: The unique DynECT system id for this DSF Monitor dsf_monitor_id = ImmutableAttribute('dsf_monitor_id') + + #: A unique label used to describe this DSF Monitor label = StringAttribute('label') + + #: The protocol for this monitor to use while monitoring. Must be one of + #: 'HTTP', 'HTTPS', 'PING', 'SMTP', or 'TCP' protocol = ValidatedAttribute('protocol', validator=('HTTP', 'HTTPS', 'PING', 'SMTP', 'TCP')) + + #: The number of responses to determine whether or not the endpoint is + #: 'up' or 'down' response_count = IntegerAttribute('response_count') + + #: How often to this DSF Monitor should monitor probe_interval = IntegerAttribute('probe_interval') + + #: How many retries this :class:`DSFMonitor` should attempt on failure + #: before giving up. retries = IntegerAttribute('retries') + + #: Indicates whether or not this DSFMonitor is active active = StringAttribute('active') + + #: Additional options pertaining to this DSF Monitor options = APIDescriptor('options') + + #: A List of :class:`DSFMonitorEndpoint`'s that are associated with this + #: DSFMonitor endpoints = ListAttribute('endpoints') def _get(self, monitor_id): @@ -1389,7 +1478,6 @@ def _post(self, label, protocol, response_count, probe_interval, retries, active='Y', timeout=None, port=None, path=None, host=None, header=None, expected=None, endpoints=None): """Create a new :class:`DSFMonitor` on the DynECT System""" - uri = '/DSFMonitor/' self._active = Active(active) self._options = {} if timeout: @@ -1411,7 +1499,8 @@ def _post(self, label, protocol, response_count, probe_interval, retries, 'options': self._options} if self._endpoints is not None: api_args['endpoints'] = [x.to_json() for x in self._endpoints] - response = DynectSession.get_session().execute(uri, 'POST', api_args) + response = DynectSession.get_session().execute(self.uri, 'POST', + api_args) self._build(response['data']) def _build(self, data): From 054b35e7377b626e91a83a2399099686fe29e67f Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Mon, 29 Dec 2014 13:18:31 -0500 Subject: [PATCH 45/59] Updated some documentation --- dyn/tm/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dyn/tm/utils.py b/dyn/tm/utils.py index b2f7d95..931619f 100644 --- a/dyn/tm/utils.py +++ b/dyn/tm/utils.py @@ -124,8 +124,8 @@ def __nonzero__(self): """ return self.value - # For forwards compatibility with Python 3.x where __bool__ is called when - # evaluating boolean expressions + # For Python 2.x/3.x compatibility where __bool__ or __nonzero__ can be + # called when evaluating boolean expressions __bool__ = __nonzero__ def __str__(self): From eb850a7ce99b147088f14ec42802f123b0782f17 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Mon, 29 Dec 2014 15:07:34 -0500 Subject: [PATCH 46/59] Updated docs. Applied some small bug fixes. --- dyn/tm/accounts.py | 211 ++++++++++++++++++++++++--------------------- 1 file changed, 112 insertions(+), 99 deletions(-) diff --git a/dyn/tm/accounts.py b/dyn/tm/accounts.py index eec0381..bb93a09 100644 --- a/dyn/tm/accounts.py +++ b/dyn/tm/accounts.py @@ -3,9 +3,9 @@ REST API """ from .errors import DynectInvalidArgumentError -from .session import DynectSession -from ..core import (APIObject, ImmutableAttribute, StringAttribute, - ListAttribute, ValidatedAttribute) +from .session import DynectSession, DNSAPIObject +from ..core import (ImmutableAttribute, StringAttribute, ListAttribute, + ValidatedAttribute) from ..compat import force_unicode __author__ = 'jnappi' @@ -15,16 +15,17 @@ def get_updateusers(search=None): - """Return a ``list`` of :class:`~dyn.tm.accounts.UpdateUser` objects. If - *search* is specified, then only :class:`~dyn.tm.accounts.UpdateUsers` who - match those search criteria will be returned in the list. Otherwise, all - :class:`~dyn.tm.accounts.UpdateUsers`'s will be returned. - - :param search: A ``dict`` of search criteria. Key's in this ``dict`` much - map to an attribute a :class:`~dyn.tm.accounts.UpdateUsers` instance - and the value mapped to by that key will be used as the search criteria - for that key when searching. - :return: a ``list`` of :class:`~dyn.tm.accounts.UpdateUser` objects + """Return a :const:`list` of :class:`~dyn.tm.accounts.UpdateUser` objects. + If *search* is specified, then only :class:`~dyn.tm.accounts.UpdateUsers` + who match those search criteria will be returned in the list. Otherwise, + all :class:`~dyn.tm.accounts.UpdateUsers`'s will be returned. + + :param search: A :const:`dict` of search criteria. Key's in this + :const:`dict` must map to an attribute a + :class:`~dyn.tm.accounts.UpdateUsers` instance and the value mapped to + by that key will be used as the search criteria for that key when + searching. + :return: a :const:`list` of :class:`~dyn.tm.accounts.UpdateUser` objects """ uri = '/UpdateUser/' api_args = {'detail': 'Y'} @@ -43,16 +44,16 @@ def get_updateusers(search=None): def get_users(search=None): - """Return a ``list`` of :class:`~dyn.tm.accounts.User` objects. If *search* - is specified, then only users who match those search parameters will be - returned in the list. Otherwise, all :class:`~dyn.tm.accounts.User`'s will - be returned. - - :param search: A ``dict`` of search criteria. Key's in this ``dict`` much - map to an attribute a :class:`~dyn.tm.accounts.User` instance and the - value mapped to by that key will be used as the search criteria for - that key when searching. - :return: a ``list`` of :class:`~dyn.tm.accounts.User` objects + """Return a :const:`list` of :class:`~dyn.tm.accounts.User` objects. If + *search* is specified, then only users who match those search parameters + will be returned in the list. Otherwise, all + :class:`~dyn.tm.accounts.User`'s will be returned. + + :param search: A :const:`dict` of search criteria. Key's in this + :const:`dict` must map to an attribute a :class:`~dyn.tm.accounts.User` + instance and the value mapped to by that key will be used as the search + criteria for that key when searching. + :return: a :const:`list` of :class:`~dyn.tm.accounts.User` objects """ uri = '/User/' api_args = {'detail': 'Y'} @@ -76,17 +77,20 @@ def get_users(search=None): def get_permissions_groups(search=None): - """Return a ``list`` of :class:`~dyn.tm.accounts.PermissionGroup` objects. - If *search* is specified, then only + """Return a :const:`list` of :class:`~dyn.tm.accounts.PermissionGroup` + objects. If *search* is specified, then only :class:`~dyn.tm.accounts.PermissionGroup`'s that match those search criteria will be returned in the list. Otherwise, all :class:`~dyn.tm.accounts.PermissionGroup`'s will be returned. - :param search: A ``dict`` of search criteria. Key's in this ``dict`` much - map to an attribute a :class:`~dyn.tm.accounts.PermissionGroup` - instance and the value mapped to by that key will be used as the search - criteria for that key when searching. - :return: a ``list`` of :class:`~dyn.tm.accounts.PermissionGroup` objects""" + :param search: A :const:`dict` of search criteria. Key's in this + :const:`dict` must map to an attribute a + :class:`~dyn.tm.accounts.PermissionGroup` instance and the value mapped + to by that key will be used as the search criteria for that key when + searching. + :return: a :const:`list` of :class:`~dyn.tm.accounts.PermissionGroup` + objects + """ uri = '/PermissionGroup/' api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) @@ -104,16 +108,18 @@ def get_permissions_groups(search=None): def get_contacts(search=None): - """Return a ``list`` of :class:`~dyn.tm.accounts.Contact` objects. If + """Return a `:const:`list` of :class:`~dyn.tm.accounts.Contact` objects. If *search* is specified, then only :class:`~dyn.tm.accounts.Contact`'s who match those search criteria will be returned in the list. Otherwise, all :class:`~dyn.tm.accounts.Contact`'s will be returned. - :param search: A ``dict`` of search criteria. Key's in this ``dict`` much - map to an attribute a :class:`~dyn.tm.accounts.Contact` instance and - the value mapped to by that key will be used as the search criteria - for that key when searching. - :return: a ``list`` of :class:`~dyn.tm.accounts.Contact` objects""" + :param search: A :const:`dict` of search criteria. Key's in this + :const:`dict` must map to an attribute a + :class:`~dyn.tm.accounts.Contact` instance and the value mapped to by + that key will be used as the search criteria for that key when + searching. + :return: a :const:`list` of :class:`~dyn.tm.accounts.Contact` objects + """ uri = '/Contact/' api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) @@ -134,16 +140,18 @@ def get_contacts(search=None): def get_notifiers(search=None): - """Return a ``list`` of :class:`~dyn.tm.accounts.Notifier` objects. If + """Return a :const:`list` of :class:`~dyn.tm.accounts.Notifier` objects. If *search* is specified, then only :class:`~dyn.tm.accounts.Notifier`'s who match those search criteria will be returned in the list. Otherwise, all :class:`~dyn.tm.accounts.Notifier`'s will be returned. - :param search: A ``dict`` of search criteria. Key's in this ``dict`` much - map to an attribute a :class:`~dyn.tm.accounts.Notifier` instance and - the value mapped to by that key will be used as the search criteria for - that key when searching. - :return: a ``list`` of :class:`~dyn.tm.accounts.Notifier` objects""" + :param search: A :const:`dict` of search criteria. Key's in this + :const:`dict` must map to an attribute a + :class:`~dyn.tm.accounts.Notifier` instance and the value mapped to by + that key will be used as the search criteria for that key when + searching. + :return: a :const:`list` of :class:`~dyn.tm.accounts.Notifier` objects + """ uri = '/Notifier/' api_args = {'detail': 'Y'} response = DynectSession.get_session().execute(uri, 'GET', api_args) @@ -160,14 +168,15 @@ def get_notifiers(search=None): return notifiers -class UpdateUser(APIObject): +class UpdateUser(DNSAPIObject): """:class:`~dyn.tm.accounts.UpdateUser` type objects are a special form of a :class:`~dyn.tm.accounts.User` which are tied to a specific Dynamic DNS services. """ + _get_length = 1 + #: UpdateUser URI uri = '/UpdateUser/' - session_type = DynectSession #: This UpdateUser's user_name. An UpdateUser's user_name is a read-only #: property which can not be updated after the UpdateUser has been created. @@ -185,6 +194,10 @@ class UpdateUser(APIObject): #: System, while active UpdateUser's are. status = ImmutableAttribute('status') + def __init__(self, *args, **kwargs): + super(UpdateUser, self).__init__(*args, **kwargs) + self.uri = '/UpdateUser/{0}/'.format(self._user_name) + def _post(self, nickname, password): """Create a new :class:`~dyn.tm.accounts.UpdateUser` on the DynECT System @@ -193,14 +206,13 @@ def _post(self, nickname, password): response = DynectSession.get_session().execute(self.uri, 'POST', api_args) self._build(response['data']) - self.uri = '/UpdateUser/{}/'.format(self._user_name) def _get(self, user_name): """Get an existing :class:`~dyn.tm.accounts.UpdateUser` from the DynECT System """ self._user_name = user_name - self.uri = '/UpdateUser/{}/'.format(self._user_name) + self.uri = '/UpdateUser/{0}/'.format(user_name) response = DynectSession.get_session().execute(self.uri, 'GET') self._build(response['data']) @@ -237,13 +249,11 @@ def __str__(self): # noinspection PyAttributeOutsideInit,PyUnresolvedReferences -class User(APIObject): +class User(DNSAPIObject): """DynECT System User object""" #: DynECT User URI uri = '/User/{user_name}/' - session_type = DynectSession - #: This User's user_name. This is a read-only property. user_name = ImmutableAttribute('user_name') @@ -256,7 +266,10 @@ class User(APIObject): #: The nickname asociated with this User nickname = StringAttribute('nickname') - #: + #: This User's Email address + email = StringAttribute('email') + + #: The password for this user password = StringAttribute('password') #: The organization this User belongs to @@ -341,13 +354,13 @@ def __init__(self, user_name, *args, **kwargs): :class:`~dyn.tm.accounts.User` should receive messages destined for a pager :param post_code: Zip code or Postal code - :param group_name: A list of permission groups this + :param group_name: A :const:`list` of permission groups this :class:`~dyn.tm.accounts.User` belongs to - :param permission: A list of permissions assigned to this + :param permission: A :const:`list` of permissions assigned to this :class:`~dyn.tm.accounts.User` - :param zone: A list of zones where this + :param zone: A :const:`list` of zones where this :class:`~dyn.tm.accounts.User`'s permissions apply - :param forbid: A list of forbidden permissions for this + :param forbid: A :const:`list` of forbidden permissions for this :class:`~dyn.tm.accounts.User` :param status: Current status of this :class:`~dyn.tm.accounts.User` :param website: This :class:`~dyn.tm.accounts.User`'s website @@ -411,12 +424,12 @@ def add_permission(self, permission): DynectSession.get_session().execute(uri, 'POST') def replace_permissions(self, permissions=None): - """Replaces the list of permissions for this + """Replaces the :const:`list` of permissions for this :class:`~dyn.tm.accounts.User` - :param permissions: A list of permissions. Pass an empty list or omit - the argument to clear the list of permissions of the - :class:`~dyn.tm.accounts.User` + :param permissions: A :const:`list` of permissions. Pass an empty + :const:`list` or omit the argument to clear the :const:`list` of + permissions of the :class:`~dyn.tm.accounts.User` """ api_args = {} if permissions is not None: @@ -449,12 +462,12 @@ def add_permissions_group(self, group): DynectSession.get_session().execute(uri, 'POST') def replace_permissions_group(self, groups=None): - """Replaces the list of permissions for this + """Replaces the :const:`list` of permissions for this :class:`~dyn.tm.accounts.User` - :param groups: A list of permissions groups. Pass an empty list or omit - the argument to clear the list of permissions groups of the - :class:`~dyn.tm.accounts.User` + :param groups: A :const:`list` of permissions groups. Pass an empty + :const:`list` or omit the argument to clear the :const:`list` of + permissions groups of the :class:`~dyn.tm.accounts.User` """ api_args = {} if groups is not None: @@ -483,7 +496,7 @@ def add_forbid_rule(self, permission, zone=None): :param permission: the permission to forbid from this :class:`~dyn.tm.accounts.User` - :param zone: A list of zones where the forbid rule applies + :param zone: A :const:`list` of zones where the forbid rule applies """ api_args = {} if zone is not None: @@ -492,13 +505,13 @@ def add_forbid_rule(self, permission, zone=None): DynectSession.get_session().execute(uri, 'POST', api_args) def replace_forbid_rules(self, forbid=None): - """Replaces the list of forbidden permissions in the + """Replaces the :const:`list` of forbidden permissions in the :class:`~dyn.tm.accounts.User`'s permissions group with a new list. - :param forbid: A list of rules to replace the forbidden rules on the - :class:`~dyn.tm.accounts.User`'s permission group. If empty or not - passed in, the :class:`~dyn.tm.accounts.User`'s forbid list will be - cleared + :param forbid: A :const:`list` of rules to replace the forbidden rules + on the :class:`~dyn.tm.accounts.User`'s permission group. If empty + or not passed in, the :class:`~dyn.tm.accounts.User`'s forbid + :const:`list` will be cleared """ api_args = {} if forbid is not None: @@ -511,7 +524,7 @@ def delete_forbid_rule(self, permission, zone=None): :class:`~dyn.tm.accounts.User`'s permission group :param permission: permission - :param zone: A list of zones where the forbid rule applies + :param zone: A :const:`list` of zones where the forbid rule applies """ api_args = {} if zone is not None: @@ -523,11 +536,10 @@ def __str__(self): return force_unicode(': {0}').format(self.user_name) -class PermissionsGroup(APIObject): +class PermissionsGroup(DNSAPIObject): """A DynECT System Permissions Group object""" #: The DynECT Permissions Group URI uri = '/PermissionGroup/{group_name}/' - session_type = DynectSession #: The name of this Permissions Group group_name = StringAttribute('group_name') @@ -543,16 +555,18 @@ class PermissionsGroup(APIObject): #: user_name is passed in all_users = StringAttribute('all_users') - #: A list of permissions to apply to this Permissions Group - permission = StringAttribute('permission') + #: A :const:`list` of permissions to apply to this Permissions Group + permission = ListAttribute('permission') - #: A list of users who belong to this Permissions Group + #: A :const:`list` of users who belong to this Permissions Group user_name = StringAttribute('user_name') - #: A list of Permissions Group's that belong to this Permissions Group + #: A :const:`list` of Permissions Group's that belong to this Permissions + #: Group subgroup = StringAttribute('subgroup') - #: A list of zones where this Permissions Group's permissions apply + #: A :const:`list` of zones where this Permissions Group's permissions + #: apply zone = ListAttribute('zone') def __init__(self, group_name, *args, **kwargs): @@ -564,10 +578,14 @@ def __init__(self, group_name, *args, **kwargs): plain or default :param all_users: If 'Y', all current users will be added to the group. Cannot be used if user_name is passed in - :param permission: A list of permissions that the group contains - :param user_name: A list of users that belong to the permission group - :param subgroup: A list of groups that belong to the permission group - :param zone: A list of zones where the group's permissions apply + :param permission: A :const:`list` of permissions that the group + contains + :param user_name: A :const:`list` of users that belong to the + permission group + :param subgroup: A :const:`list` of groups that belong to the + permission group + :param zone: A :const:`list` of zones where the group's permissions + apply """ self.uri = self.uri.format(group_name=group_name) self._group_name = group_name @@ -609,6 +627,7 @@ def _build(self, data): for zone in zones: self._zone.append(zone['zone_name']) super(PermissionsGroup, self)._build(data) + self.uri = '/PermissionGroup/{0}/'.format(self.group_name) def _get(self): """Get an existing :class:`~dyn.tm.accounts.PermissionsGroup` from the @@ -621,7 +640,7 @@ def _update(self, **api_args): """Update this object on the DynECT System""" if 'group_name' in api_args: api_args['new_group_name'] = api_args.pop('group_name') - super(PermissionsGroup, self)._update(group_name=self.group_name, + super(PermissionsGroup, self)._update(#group_name=self.group_name, **api_args) def add_permission(self, permission): @@ -635,10 +654,11 @@ def add_permission(self, permission): self._permission.append(permission) def replace_permissions(self, permission=None): - """Replaces a list of individual user permissions for the user + """Replaces a :const:`list` of individual user permissions for the user - :param permission: A list of permissions. Pass an empty list or omit - the argument to clear the list of permissions of the user + :param permission: A :const:`list` of permissions. Pass an empty + :const:`list` or omit the argument to clear the :const:`list` of + permissions of the user """ api_args = {} if permission is not None: @@ -716,11 +736,10 @@ def __str__(self): # noinspection PyUnresolvedReferences,PyMissingConstructor -class Notifier(APIObject): +class Notifier(DNSAPIObject): """DynECT System Notifier""" #: The DynECT Notifier URI uri = '/Notifier/' - session_type = DynectSession #: The unique DynECT system id for this Notifier notifier_id = ImmutableAttribute('notifier_id') @@ -728,10 +747,10 @@ class Notifier(APIObject): #: A unique label for this Notifier label = StringAttribute('label') - #: A list of recipients attached to this Notifier + #: A :const:`list` of recipients attached to this Notifier recipients = ListAttribute('recipients') - #: A list of services that this Notifier is attached to + #: A :const:`list` of services that this Notifier is attached to services = ListAttribute('services') def __init__(self, *args, **kwargs): @@ -782,11 +801,10 @@ def __str__(self): return force_unicode(': {0}').format(self.label) -class Contact(APIObject): +class Contact(DNSAPIObject): """A DynECT System Contact""" #: The DynECT Contact URI uri = '/Contact/{nickname}/' - session_type = DynectSession #: The nickname for this Contact nickname = StringAttribute('nickname') @@ -870,14 +888,8 @@ def __init__(self, nickname, *args, **kwargs): :param website: The :class:`~dyn.tm.accounts.Contact`'s website """ self.uri = self.uri.format(nickname=nickname) - super(Contact, self).__init__() - if 'api' in kwargs: - del kwargs['api'] - self._build(kwargs) - elif len(args) == 0 and len(kwargs) == 0: - self._get() - else: - self._post(*args, **kwargs) + self._nickname = nickname + super(Contact, self).__init__(*args, **kwargs) def _post(self, email, first_name, last_name, organization, address=None, address_2=None, city=None, country=None, fax=None, @@ -905,8 +917,9 @@ def _post(self, email, first_name, last_name, organization, address=None, def _build(self, data): if '_nickname' in data: - setattr(self, 'nickname', data.pop('_nickname')) + setattr(self, '_nickname', data.pop('_nickname')) super(Contact, self)._build(data) + self.uri = '/Contact/{0}/'.format(self._nickname) def _update(self, **api_args): if 'nickname' in api_args: From 52e1a53620c3390de7c4ea49fb65ad8692ef9108 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Mon, 29 Dec 2014 15:07:59 -0500 Subject: [PATCH 47/59] Adding an APIObject subclass with a default session_type of DynectSession --- dyn/tm/session.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dyn/tm/session.py b/dyn/tm/session.py index 217084d..454b6c6 100644 --- a/dyn/tm/session.py +++ b/dyn/tm/session.py @@ -5,8 +5,8 @@ own respective functionality. """ # API Libs -from ..core import SessionEngine from .errors import * +from ..core import SessionEngine, APIObject from ..compat import force_unicode from ..encrypt import AESCipher @@ -164,3 +164,10 @@ def __str__(self): header = super(DynectSession, self).__str__() return header + force_unicode(': {}, {}').format(self.customer, self.username) + + +class DNSAPIObject(APIObject): + """:class:`APIObject` subclass with a default `session_type` of + :class:`~dyn.tm.session.DynectSession` + """ + session_type = DynectSession From 09f3e88360ab8a5ba0c478aaf8b2a8599d2e067f Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Mon, 29 Dec 2014 15:08:31 -0500 Subject: [PATCH 48/59] Updating docs for __bool__ = __nonzero__ --- dyn/tm/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dyn/tm/utils.py b/dyn/tm/utils.py index 931619f..6561e05 100644 --- a/dyn/tm/utils.py +++ b/dyn/tm/utils.py @@ -124,7 +124,7 @@ def __nonzero__(self): """ return self.value - # For Python 2.x/3.x compatibility where __bool__ or __nonzero__ can be + # For Python 2.x/3.x compatibility where __bool__ or __nonzero__ can be # called when evaluating boolean expressions __bool__ = __nonzero__ From 4eee72acd9687282ba3e4f18f7d625a6513d3f85 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 7 Mar 2015 13:21:02 -0500 Subject: [PATCH 49/59] Adding convenience session execute methods --- dyn/core.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/dyn/core.py b/dyn/core.py index 9790688..344022e 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -354,6 +354,26 @@ def get_session(cls): key = getattr(cls, '__metakey__') return cls._instances.get(key, {}).get(cur_thread, None) + @classmethod + def get(cls, url, api_args=None): + """Wrap a raw session execute call with a GET convenience method""" + return cls.get_session().execute(url, 'GET', api_args) + + @classmethod + def post(cls, url, api_args=None): + """Wrap a raw session execute call with a POST convenience method""" + return cls.get_session().execute(url, 'POST', api_args) + + @classmethod + def put(cls, url, api_args=None): + """Wrap a raw session execute call with a PUT convenience method""" + return cls.get_session().execute(url, 'PUT', api_args) + + @classmethod + def delete(cls, url, api_args=None): + """Wrap a raw session execute call with a DELETE convenience method""" + return cls.get_session().execute(url, 'DELETE', api_args) + @classmethod def close_session(cls): """Remove the current session from the dict of instances and return it. From c0233b5f4931ca3c2f0d0f1e00579c5031ab652e Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 7 Mar 2015 13:22:43 -0500 Subject: [PATCH 50/59] Updating HTTPRedirect to use new APIObject structure --- dyn/tm/services/httpredirect.py | 152 +++++++++----------------------- 1 file changed, 42 insertions(+), 110 deletions(-) diff --git a/dyn/tm/services/httpredirect.py b/dyn/tm/services/httpredirect.py index 4f64bad..f1e1ff3 100644 --- a/dyn/tm/services/httpredirect.py +++ b/dyn/tm/services/httpredirect.py @@ -1,136 +1,68 @@ # -*- coding: utf-8 -*- -"""This module contains API Wrapper implementations of the HTTP Redirect service +"""This module contains API Wrapper implementations of the HTTP Redirect +service """ -import logging - -from ..session import DynectSession +from ...core import (ImmutableAttribute, ValidatedAttribute, StringAttribute, + BooleanAttribute) +from ..session import DynectSession, DNSAPIObject from ...compat import force_unicode __author__ = 'xorg' __all__ = ['HTTPRedirect'] -class HTTPRedirect(object): +class HTTPRedirect(DNSAPIObject): """HTTPRedirect is a service which sets up a redirect to the specified URL.// """ + uri = '/HTTPRedirect/{zone}/{fqdn}/' + + #: The zone that this HTTPRedirect Service is attached to + zone = ImmutableAttribute('zone') + + #: The FQDN of the node where this service is attached + fqdn = ImmutableAttribute('fqdn') + + #: HTTP response code to return for redirection + code = ValidatedAttribute('code', default=302, validator=(301, 302)) + + #: The target URL where the client is sent. Must begin with either http:// + #: or https:// + url = StringAttribute('url') + + #: A flag indicating whether the redirection should include the originally + #: requested URI. + keep_uri = BooleanAttribute('keep_uri', default=True) + def __init__(self, zone, fqdn, *args, **kwargs): """Create a new :class:`HTTPRedirect` service object :param zone: The zone to attach this HTTPRedirect Service to :param fqdn: The FQDN of the node where this service will be attached :param code: HTTP response code to return for redirection. - :param url: The target URL where the client is sent. Must begin with either http:// or https:// - :param keep_uri: A flag indicating whether the redirection should include the originally requested URI. + :param url: The target URL where the client is sent. Must begin with + either http:// or https:// + :param keep_uri: A flag indicating whether the redirection should + include the originally requested URI """ - super(HTTPRedirect, self).__init__() - self._zone = zone - self._fqdn = fqdn - self._code = self._url = self.keep_uri = None - if 'api' in kwargs: - del kwargs['api'] - for key, val in kwargs.items(): - setattr(self, '_' + key, val) - elif len(args) + len(kwargs) == 1: - self._get() - else: - self._post(*args, **kwargs) + self._zone, self._fqdn = zone, fqdn + self.uri = self.uri.format(zone=zone, fqdn=fqdn) + super(HTTPRedirect, self).__init__(*args, **kwargs) def _get(self): """Build an object around an existing DynECT HTTPRedirect Service""" - self.uri = '/HTTPRedirect/{}/{}/'.format(self._zone, self._fqdn) api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(self.uri, 'GET', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) + response = DynectSession.get(self.uri, api_args) + self._build(response['data']) - def _post(self, code, keep_uri, url): + def _post(self, url, code=302, keep_uri=True): """Create a new HTTPRedirect Service on the DynECT System""" - self._code = code - self._keep_uri = keep_uri - self._url = url - self.uri = '/HTTPRedirect/{}/{}/'.format(self._zone, self._fqdn) - api_args = {'code': self._code, 'keep_uri': self._keep_uri, 'url': self._url} - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - def _update(self, code, keep_uri, url): - """Update an existing HTTPRedirect Service on the DynECT System""" - self._code = code - self._keep_uri = keep_uri - self._url = url - self.uri = '/HTTPRedirect/{}/{}/'.format(self._zone, self._fqdn) - api_args = {'code': self._code, 'keep_uri': self._keep_uri, 'url': self._url} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) - for key, val in response['data'].items(): - setattr(self, '_' + key, val) - - - - @property - def zone(self): - """The zone that this HTTPRedirect Service is attached to is a read-only - attribute - """ - return self._zone - @zone.setter - def zone(self, value): - pass - - @property - def fqdn(self): - """The fqdn that this HTTPRedirect Service is attached to is a read-only - attribute - """ - return self._fqdn - @fqdn.setter - def fqdn(self, value): - pass - - @property - def code(self): - """HTTP response code to return for redirection. - Valid values: - 301 – Permanent redirect - 302 – Temporary redirect - """ - return self._code - @code.setter - def code(self, value): - pass - - @property - def keep_uri(self): - """A flag indicating whether the redirection should include the originally requested URI. - Valid values: Y, N - """ - return self._keep_uri - @keep_uri.setter - def keep_uri(self, value): - pass - - @property - def url(self): - """The target URL where the client is sent. Must begin with either http:// or https://""" - return self._url - @url.setter - def url(self, value): - pass - - - def delete(self): - """Delete this HTTPRedirect service from the DynECT System""" - api_args = {} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + self._url, self._code, self._keep_uri = url, code, keep_uri + api_args = {'code': self._code, + 'keep_uri': self._keep_uri, + 'url': self._url} + response = DynectSession.post(self.uri, api_args) + self._build(response['data']) def __str__(self): """str override""" - return force_unicode(': {}').format(self._fqdn) - __repr__ = __unicode__ = __str__ - - def __bytes__(self): - """bytes override""" - return bytes(self.__str__()) + return force_unicode(': {0}').format(self._fqdn) From f06bb3a2bd5ea0f5342d962c9828d7713be37d0e Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 7 Mar 2015 13:41:26 -0500 Subject: [PATCH 51/59] Updated all DynectSession calls to use new convenience http methods --- dyn/tm/accounts.py | 72 +++++++++++++++--------------- dyn/tm/records.py | 7 ++- dyn/tm/reports.py | 18 +++----- dyn/tm/services/_shared.py | 2 +- dyn/tm/services/active_failover.py | 3 +- dyn/tm/services/ddns.py | 2 +- dyn/tm/services/dnssec.py | 7 ++- dyn/tm/services/dsf.py | 37 ++++++++------- dyn/tm/services/gslb.py | 10 ++--- dyn/tm/services/reversedns.py | 7 ++- dyn/tm/services/rttm.py | 17 +++---- dyn/tm/zones.py | 54 +++++++++++----------- 12 files changed, 107 insertions(+), 129 deletions(-) diff --git a/dyn/tm/accounts.py b/dyn/tm/accounts.py index bb93a09..9d7edb2 100644 --- a/dyn/tm/accounts.py +++ b/dyn/tm/accounts.py @@ -29,7 +29,7 @@ def get_updateusers(search=None): """ uri = '/UpdateUser/' api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) update_users = [] for user in response['data']: update_users.append(UpdateUser(api=False, **user)) @@ -65,7 +65,7 @@ def get_users(search=None): else: search_string = '{}:"{}"'.format(key, val) api_args['search'] = search_string - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) users = [] for user in response['data']: user_name = None @@ -93,7 +93,7 @@ def get_permissions_groups(search=None): """ uri = '/PermissionGroup/' api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) groups = [] for group in response['data']: groups.append(PermissionsGroup(None, api=False, **group)) @@ -122,7 +122,7 @@ def get_contacts(search=None): """ uri = '/Contact/' api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) contacts = [] for contact in response['data']: if 'nickname' in contact: @@ -154,7 +154,7 @@ def get_notifiers(search=None): """ uri = '/Notifier/' api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) notifiers = [] for notifier in response['data']: notifiers.append(Notifier(None, api=False, **notifier)) @@ -203,8 +203,7 @@ def _post(self, nickname, password): System """ api_args = {'nickname': nickname, 'password': password} - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) + response = DynectSession.post(self.uri, api_args) self._build(response['data']) def _get(self, user_name): @@ -213,7 +212,7 @@ def _get(self, user_name): """ self._user_name = user_name self.uri = '/UpdateUser/{0}/'.format(user_name) - response = DynectSession.get_session().execute(self.uri, 'GET') + response = DynectSession.get(self.uri) self._build(response['data']) def block(self): @@ -242,7 +241,7 @@ def delete(self): """Delete this :class:`~dyn.tm.accounts.UpdateUser` from the DynECT System. It is important to note that this operation can not be undone. """ - DynectSession.get_session().execute(self.uri, 'DELETE') + DynectSession.delete(self.uri) def __str__(self): return force_unicode(': {0}').format(self.user_name) @@ -400,7 +399,7 @@ def _post(self, password, email, first_name, last_name, nickname, self._forbid = forbid self._status = status self._website = website - response = DynectSession.get_session().execute(self.uri, 'POST', self) + response = DynectSession.post(self.uri, self) self._build(response['data']) def block(self): @@ -421,7 +420,7 @@ def add_permission(self, permission): self.permissions.append(permission) uri = '/UserPermissionEntry/{0}/{1}/'.format(self._user_name, permission) - DynectSession.get_session().execute(uri, 'POST') + DynectSession.post(uri) def replace_permissions(self, permissions=None): """Replaces the :const:`list` of permissions for this @@ -438,7 +437,7 @@ def replace_permissions(self, permissions=None): else: self.permissions = [] uri = '/UserPermissionEntry/{}/'.format(self._user_name) - DynectSession.get_session().execute(uri, 'PUT', api_args) + DynectSession.put(uri, api_args) def delete_permission(self, permission): """Remove this specific permission from the @@ -449,7 +448,7 @@ def delete_permission(self, permission): if permission in self.permissions: self.permissions.remove(permission) uri = '/UserPermissionEntry/{}/{}/'.format(self._user_name, permission) - DynectSession.get_session().execute(uri, 'DELETE') + DynectSession.delete(uri) def add_permissions_group(self, group): """Assigns the permissions group to this :class:`~dyn.tm.accounts.User` @@ -458,8 +457,8 @@ def add_permissions_group(self, group): :class:`~dyn.tm.accounts.User` """ self.permission_groups.append(group) - uri = '/UserGroupEntry/{}/{}/'.format(self._user_name, group) - DynectSession.get_session().execute(uri, 'POST') + uri = '/UserGroupEntry/{0}/{1}/'.format(self._user_name, group) + DynectSession.post(uri) def replace_permissions_group(self, groups=None): """Replaces the :const:`list` of permissions for this @@ -475,8 +474,8 @@ def replace_permissions_group(self, groups=None): self.groups = groups else: self.groups = [] - uri = '/UserGroupEntry/{}/'.format(self._user_name) - DynectSession.get_session().execute(uri, 'PUT', api_args) + uri = '/UserGroupEntry/{0}/'.format(self._user_name) + DynectSession.put(uri, api_args) def delete_permissions_group(self, group): """Removes the permissions group from the @@ -488,7 +487,7 @@ def delete_permissions_group(self, group): if group in self.permissions: self.permission_groups.remove(group) uri = '/UserGroupEntry/{}/{}/'.format(self._user_name, group) - DynectSession.get_session().execute(uri, 'DELETE') + DynectSession.delete(uri) def add_forbid_rule(self, permission, zone=None): """Adds the forbid rule to the :class:`~dyn.tm.accounts.User`'s @@ -501,8 +500,8 @@ def add_forbid_rule(self, permission, zone=None): api_args = {} if zone is not None: api_args['zone'] = zone - uri = '/UserForbidEntry/{}/{}/'.format(self._user_name, permission) - DynectSession.get_session().execute(uri, 'POST', api_args) + uri = '/UserForbidEntry/{0}/{1}/'.format(self._user_name, permission) + DynectSession.post(uri, api_args) def replace_forbid_rules(self, forbid=None): """Replaces the :const:`list` of forbidden permissions in the @@ -516,8 +515,8 @@ def replace_forbid_rules(self, forbid=None): api_args = {} if forbid is not None: api_args['forbid'] = forbid - uri = '/UserForbidEntry/{}/'.format(self._user_name) - DynectSession.get_session().execute(uri, 'PUT', api_args) + uri = '/UserForbidEntry/{0}/'.format(self._user_name) + DynectSession.put(uri, api_args) def delete_forbid_rule(self, permission, zone=None): """Removes a forbid permissions rule from the @@ -529,8 +528,8 @@ def delete_forbid_rule(self, permission, zone=None): api_args = {} if zone is not None: api_args['zone'] = zone - uri = '/UserForbidEntry/{}/{}/'.format(self._user_name, permission) - DynectSession.get_session().execute(uri, 'DELETE', api_args) + uri = '/UserForbidEntry/{0}/{1}/'.format(self._user_name, permission) + DynectSession.delete(uri, api_args) def __str__(self): return force_unicode(': {0}').format(self.user_name) @@ -612,8 +611,7 @@ def _post(self, description, group_type=None, all_users=None, api_args['type'] = val else: api_args[key[1:]] = val - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) + response = DynectSession.post(self.uri, api_args) self._build(response['data']) def _build(self, data): @@ -633,7 +631,7 @@ def _get(self): """Get an existing :class:`~dyn.tm.accounts.PermissionsGroup` from the DynECT System """ - response = DynectSession.get_session().execute(self.uri, 'GET') + response = DynectSession.get(self.uri) self._build(response['data']) def _update(self, **api_args): @@ -650,7 +648,7 @@ def add_permission(self, permission): """ uri = '/PermissionGroupPermissionEntry/{0}/{1}/'.format( self._group_name, permission) - DynectSession.get_session().execute(uri, 'POST') + DynectSession.post(uri) self._permission.append(permission) def replace_permissions(self, permission=None): @@ -664,7 +662,7 @@ def replace_permissions(self, permission=None): if permission is not None: api_args['permission'] = permission uri = '/PermissionGroupPermissionEntry/{0}/'.format(self._group_name) - DynectSession.get_session().execute(uri, 'PUT', api_args) + DynectSession.put(uri, api_args) if permission: self._permission = permission else: @@ -677,7 +675,7 @@ def remove_permission(self, permission): """ uri = '/PermissionGroupPermissionEntry/{0}/{1}/'.format( self._group_name, permission) - DynectSession.get_session().execute(uri, 'DELETE') + DynectSession.delete(uri) self._permission.remove(permission) def add_zone(self, zone, recurse='Y'): @@ -691,7 +689,7 @@ def add_zone(self, zone, recurse='Y'): api_args = {'recurse': recurse} uri = '/PermissionGroupZoneEntry/{0}/{1}/'.format(self._group_name, zone) - DynectSession.get_session().execute(uri, 'POST', api_args) + DynectSession.post(uri, api_args) self._zone.append(zone) def add_subgroup(self, name): @@ -704,7 +702,7 @@ def add_subgroup(self, name): """ uri = '/PermissionGroupSubgroupEntry/{0}/{1}/'.format(self._group_name, name) - DynectSession.get_session().execute(uri, 'POST') + DynectSession.post(uri) self._subgroup.append(name) def update_subgroup(self, subgroups): @@ -715,7 +713,7 @@ def update_subgroup(self, subgroups): """ api_args = {'subgroup': subgroups} uri = '/PermissionGroupSubgroupEntry/{0}/'.format(self._group_name) - DynectSession.get_session().execute(uri, 'PUT', api_args) + DynectSession.put(uri, api_args) self._subgroup = subgroups def delete_subgroup(self, name): @@ -728,7 +726,7 @@ def delete_subgroup(self, name): """ uri = '/PermissionGroupSubgroupEntry/{0}/{1}/'.format(self._group_name, name) - DynectSession.get_session().execute(uri, 'DELETE') + DynectSession.delete(uri) self._subgroup.remove(name) def __str__(self): @@ -786,7 +784,7 @@ def _post(self, label=None, recipients=None, services=None): self._label = label self._recipients = recipients self._services = services - response = DynectSession.get_session().execute(uri, 'POST', self) + response = DynectSession.post(uri, self) self._build(response['data']) def _get(self, notifier_id): @@ -794,7 +792,7 @@ def _get(self, notifier_id): DynECT System """ self.uri = '/Notifier/{0}/'.format(notifier_id) - response = DynectSession.get_session().execute(self.uri, 'GET') + response = DynectSession.get(self.uri) self._build(response['data']) def __str__(self): @@ -912,7 +910,7 @@ def _post(self, email, first_name, last_name, organization, address=None, self._post_code = post_code self._state = state self._website = website - response = DynectSession.get_session().execute(self.uri, 'POST', self) + response = DynectSession.post(self.uri, self) self._build(response['data']) def _build(self, data): diff --git a/dyn/tm/records.py b/dyn/tm/records.py index a3f2974..70c87f3 100644 --- a/dyn/tm/records.py +++ b/dyn/tm/records.py @@ -59,8 +59,7 @@ def _post(self, *args, **api_args): :param api_args: arguments to be pased to the API call """ - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) + response = DynectSession.post(self.uri, api_args) self._build(response['data']) def _get(self, record_id): @@ -68,8 +67,8 @@ def _get(self, record_id): :param record_id: The id of the record you would like to get """ - uri = self.uri + '{}/'.format(record_id) - response = DynectSession.get_session().execute(uri, 'GET') + uri = self.uri + '{0}/'.format(record_id) + response = DynectSession.get(uri) self._build(response['data']) def _update(self, **api_args): diff --git a/dyn/tm/reports.py b/dyn/tm/reports.py index 11d4b74..b8feca4 100644 --- a/dyn/tm/reports.py +++ b/dyn/tm/reports.py @@ -23,8 +23,7 @@ def get_check_permission(permission, zone_name=None): api_args = {'permission': permission} if zone_name is not None: api_args['zone_name'] = zone_name - response = DynectSession.get_session().execute('/CheckPermissionReport/', - 'POST', api_args) + response = DynectSession.post('/CheckPermissionReport/', api_args) return response['data'] @@ -47,8 +46,7 @@ def get_dnssec_timeline(zone_name, start_ts=None, end_ts=None): api_args['end_ts'] = unix_date(end_ts) elif end_ts is None and start_ts is not None: api_args['end_ts'] = unix_date(datetime.now()) - response = DynectSession.get_session().execute('/DNSSECTimelineReport/', - 'POST', api_args) + response = DynectSession.post('/DNSSECTimelineReport/', api_args) return response['data'] @@ -67,8 +65,7 @@ def get_rttm_log(zone_name, fqdn, start_ts, end_ts=None): end_ts = end_ts or datetime.now() api_args = {'zone': zone_name, 'fqdn': fqdn, 'start_ts': unix_date(start_ts), 'end_ts': unix_date(end_ts)} - response = DynectSession.get_session().execute('/RTTMLogReport/', 'POST', - api_args) + response = DynectSession.post('/RTTMLogReport/', api_args) return response['data'] @@ -83,8 +80,7 @@ def get_rttm_rrset(zone_name, fqdn, ts): :return: A *dict* containing rrset report data """ api_args = {'zone': zone_name, 'fqdn': fqdn, 'ts': unix_date(ts)} - response = DynectSession.get_session().execute('/RTTMRRSetReport/', - 'POST', api_args) + response = DynectSession.post('/RTTMRRSetReport/', api_args) return response['data'] @@ -113,8 +109,7 @@ def get_qps(start_ts, end_ts=None, breakdown=None, hosts=None, rrecs=None, api_args['rrecs'] = rrecs if zones is not None: api_args['zones'] = zones - response = DynectSession.get_session().execute('/QPSReport/', 'POST', - api_args) + response = DynectSession.post('/QPSReport/', api_args) return response['data'] @@ -132,6 +127,5 @@ def get_zone_notes(zone_name, offset=None, limit=None): api_args['offset'] = offset if limit: api_args['limit'] = limit - response = DynectSession.get_session().execute('/ZoneNoteReport/', 'POST', - api_args) + response = DynectSession.post('/ZoneNoteReport/', api_args) return response['data'] diff --git a/dyn/tm/services/_shared.py b/dyn/tm/services/_shared.py index 7abc328..5067fb9 100644 --- a/dyn/tm/services/_shared.py +++ b/dyn/tm/services/_shared.py @@ -92,7 +92,7 @@ def status(self): """Get the current status of this :class:`Monitor` from the DynECT System """ - respnose = DynectSession.get_session().execute(self.uri, 'GET') + respnose = DynectSession.get(self.uri) return respnose['data']['status'] def __str__(self): diff --git a/dyn/tm/services/active_failover.py b/dyn/tm/services/active_failover.py index de9dc36..34d0d2b 100644 --- a/dyn/tm/services/active_failover.py +++ b/dyn/tm/services/active_failover.py @@ -127,8 +127,7 @@ def _post(self, address, failover_mode, failover_data, monitor, self._syslog_facility = syslog_facility self._ttl = ttl - response = DynectSession.get_session().execute(self.uri, 'POST', - self.api_args) + response = DynectSession.post(self.uri, self.api_args) self._build(response['data']) def _update(self, **api_args): diff --git a/dyn/tm/services/ddns.py b/dyn/tm/services/ddns.py index d0b00a8..4028eec 100644 --- a/dyn/tm/services/ddns.py +++ b/dyn/tm/services/ddns.py @@ -69,7 +69,7 @@ def _post(self, address, user=None): if user: api_args['user'] = user api_args['full_setup'] = True - resp = DynectSession.get_session().execute(self.uri, 'POST', api_args) + resp = DynectSession.post(self.uri, api_args) self._build(resp['data']) def reset(self): diff --git a/dyn/tm/services/dnssec.py b/dyn/tm/services/dnssec.py index d80e6a1..b44dc3c 100644 --- a/dyn/tm/services/dnssec.py +++ b/dyn/tm/services/dnssec.py @@ -15,7 +15,7 @@ def get_all_dnssec(): """:return: A ``list`` of :class:`DNSSEC` Services""" uri = '/DNSSEC/' api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) dnssecs = [] for dnssec in response['data']: zone = dnssec['zone'] @@ -126,8 +126,7 @@ def _post(self, keys, contact_nickname, notify_events=None): # Need to cast to CSV for API if self._notify_events is not None: api_args['notify_events'] = ', '.join(self._notify_events) - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) + response = DynectSession.post(self.uri, api_args) self._build(response['data']) def _build(self, data): @@ -215,7 +214,7 @@ def timeline_report(self, start_ts=None, end_ts=None): elif end_ts is None and start_ts is not None: api_args['end_ts'] = unix_date(datetime.now()) uri = '/DNSSECTimelineReport/' - response = DynectSession.get_session().execute(uri, 'POST', api_args) + response = DynectSession.post(uri, api_args) return response['data'] def __str__(self): diff --git a/dyn/tm/services/dsf.py b/dyn/tm/services/dsf.py index fd95c74..075e1bd 100644 --- a/dyn/tm/services/dsf.py +++ b/dyn/tm/services/dsf.py @@ -27,7 +27,7 @@ def get_all_dsf_services(): """:return: A ``list`` of :class:`TrafficDirector` Services""" uri = '/DSF/' api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) directors = [] for dsf in response['data']: directors.append(TrafficDirector(None, api=False, **dsf)) @@ -38,7 +38,7 @@ def get_all_dsf_monitors(): """:return: A ``list`` of :class:`DSFMonitor` Services""" uri = '/DSFMonitor/' api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) mons = [] for dsf in response['data']: mons.append(DSFMonitor(api=False, **dsf)) @@ -94,7 +94,7 @@ def _post(self, dsf_id, record_set_id): key.startswith('_'): if key != '_dsf_id' and key != '_record_set_id': api_args[key[1:]] = val - resp = DynectSession.get_session().execute(self.uri, 'POST', api_args) + resp = DynectSession.post(self.uri, api_args) self._build(resp['data']) def _get(self, dsf_id, record_set_id): @@ -106,7 +106,7 @@ def _get(self, dsf_id, record_set_id): associated with this :class:`DSFRecord` """ self.uri = '/DSFRecord/{0}/{1}/'.format(dsf_id, record_set_id) - response = DynectSession.get_session().execute(self.uri, 'GET') + response = DynectSession.get(self.uri) self._build(response['data']) def _build(self, data): @@ -144,7 +144,7 @@ def to_json(self): def delete(self): """Delete this :class:`DSFRecord`""" api_args = {'publish': 'Y'} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + DynectSession.delete(self.uri, api_args) class DSFARecord(_DSFRecord, ARecord): @@ -965,7 +965,7 @@ def _post(self, dsf_id): elif val is not None and not hasattr(val, '__call__') and \ key.startswith('_'): api_args[key[1:]] = val - response = DynectSession.get_session().execute(uri, 'POST', api_args) + response = DynectSession.post(uri, api_args) self._build(response['data']) self.uri = '/DSFRecordSet/{0}/{1}/'.format(self._service_id, self._dsf_record_set_id) @@ -982,7 +982,7 @@ def _get(self, dsf_id, dsf_record_set_id): self._dsf_record_set_id = dsf_record_set_id self.uri = '/DSFRecordSet/{0}/{1}/'.format(self._service_id, self._dsf_record_set_id) - response = DynectSession.get_session().execute(self.uri, 'GET') + response = DynectSession.get(self.uri) self._build(response['data']) def _build(self, data): @@ -1007,7 +1007,7 @@ def to_json(self): def delete(self): """Delete this :class:`DSFRecordSet` from the Dynect System""" api_args = {'publish': 'Y'} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + DynectSession.delete(self.uri, api_args) def __str__(self): return ': {1}'.format(self.rdata_class, self.label) @@ -1078,7 +1078,7 @@ def _post(self, dsf_id, dsf_response_pool_id): api_args['core'] = self._core if self._record_sets: api_args['record_sets'] = self._record_sets.to_json() - resp = DynectSession.get_session().execute(self.uri, 'POST', api_args) + resp = DynectSession.post(self.uri, api_args) self._build(resp['data']) def _get(self, dsf_id, dsf_response_pool_id): @@ -1115,7 +1115,7 @@ def to_json(self): def delete(self): """Delete this :class:`DSFFailoverChain` from the Dynect System""" api_args = {'publish': 'Y'} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + DynectSession.delete(self.uri, api_args) def __str__(self): return ': {0}'.format(self.label) @@ -1206,7 +1206,7 @@ def _post(self): api_args['index'] = self.index if self._rs_chains: api_args['rs_chains'] = self._rs_chains - resp = DynectSession.get_session().execute(self.uri, 'POST', api_args) + resp = DynectSession.post(self.uri, api_args) self._build(resp['data']) def _get(self, dsf_response_pool_id): @@ -1215,7 +1215,7 @@ def _get(self, dsf_response_pool_id): :param dsf_response_pool_id: the id of this :class:`DSFResponsePool` """ self.uri = self.uri.format(self.dsf_id, dsf_response_pool_id) - response = DynectSession.get_session().execute(self.uri, 'GET') + response = DynectSession.get(self.uri) self._build(response['data']) def _build(self, data): @@ -1239,7 +1239,7 @@ def to_json(self): def delete(self): """Delete this :class:`DSFResponsePool` from the DynECT System""" api_args = {'publish': 'Y'} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + DynectSession.delete(self.uri, api_args) def __str__(self): return ': {0}'.format(self.dsf_response_pool_id) @@ -1298,7 +1298,7 @@ def _post(self, dsf_id): api_args = {'publish': 'Y', 'label': self._label, 'criteria_type': self._criteria_type, 'criteria': self._criteria} - response = DynectSession.get_session().execute(uri, 'POST', api_args) + response = DynectSession.post(uri, api_args) self._build(response['data']) self.uri = '/DSFRuleset/{0}/{1}/'.format(self._service_id, self._dsf_ruleset_id) @@ -1333,7 +1333,7 @@ def delete(self): :class:`TrafficDirector` Service """ api_args = {'publish': 'Y'} - DynectSession.get_session().execute(self.uri, 'DELETE', api_args) + DynectSession.delete(self.uri, api_args) def __str__(self): return ': {0}'.format(self.label) @@ -1499,8 +1499,7 @@ def _post(self, label, protocol, response_count, probe_interval, retries, 'options': self._options} if self._endpoints is not None: api_args['endpoints'] = [x.to_json() for x in self._endpoints] - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) + response = DynectSession.post(self.uri, api_args) self._build(response['data']) def _build(self, data): @@ -1585,7 +1584,7 @@ def _post(self, label, ttl=None, publish='Y', nodes=None, notifiers=None, in notifiers] if rulesets: api_args['rulesets'] = [rule.to_json() for rule in rulesets] - response = DynectSession.get_session().execute(uri, 'POST', api_args) + response = DynectSession.post(uri, api_args) self.uri = '/DSF/{0}/'.format(response['data']['service_id']) self._build(response['data']) @@ -1611,7 +1610,7 @@ def _get(self, service_id): """Get an existing :class:`TrafficDirector` from the DynECT System""" self.uri = '/DSF/{0}/'.format(service_id) api_args = {'pending_changes': 'Y'} - resp = DynectSession.get_session().execute(self.uri, 'GET', api_args) + resp = DynectSession.get(self.uri, api_args) self._build(resp['data']) def _update(self, **api_args): diff --git a/dyn/tm/services/gslb.py b/dyn/tm/services/gslb.py index a77c733..868363e 100644 --- a/dyn/tm/services/gslb.py +++ b/dyn/tm/services/gslb.py @@ -61,7 +61,7 @@ def _post(self, label=None, weight=None, serve_mode=None): api_args = {'address': self.address, 'label': label, 'weight': weight, 'serve_mode': serve_mode} api_args = {api_args[k] for k in api_args if api_args[k] is not None} - response = DynectSession.get_session().execute(uri, 'POST', api_args) + response = DynectSession.post(uri, api_args) self._build(response['data']) def _update(self, **api_args): @@ -145,7 +145,7 @@ def _post(self, pool, serve_count=None, failover_mode=None, 'failover_mode': failover_mode, 'failover_data': failover_data} api_args = {api_args[k] for k in api_args if api_args[k] is not None} - response = DynectSession.get_session()(uri, 'POST', api_args) + response = DynectSession.post(uri, api_args) self._build(response['data']) def _build(self, data): @@ -256,8 +256,7 @@ def _post(self, contact_nickname, region, auto_recover=None, ttl=None, 'syslog_facility': syslog_facility, 'monitor': monitor.to_json()} api_args = {api_args[k] for k in api_args if api_args[k] is not None} - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) + response = DynectSession.post(self.uri, api_args) self._build(response['data']) def _build(self, data): @@ -307,8 +306,7 @@ def recover(self, address=None): api_args = {'recoverip': True, 'address': address} else: api_args = {'recover': True} - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) + response = DynectSession.put(self.uri, api_args) self._build(response['data']) def add_region(self, region_code, pool, serve_count=None, diff --git a/dyn/tm/services/reversedns.py b/dyn/tm/services/reversedns.py index 0713d5d..e1243f3 100644 --- a/dyn/tm/services/reversedns.py +++ b/dyn/tm/services/reversedns.py @@ -51,14 +51,13 @@ def _post(self, hosts, netmask, ttl='default', record_types=None): 'netmask': netmask} if ttl is not None: api_args['ttl'] = ttl - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) + response = DynectSession.post(self.uri, api_args) self._build(response['data']) def _get(self, service_id): """Build an object around an existing DynECT ReverseDNS Service""" - uri = '/IPTrack/{}/{}/{}/'.format(self.zone, self.fqdn, service_id) - response = DynectSession.get_session().execute(uri, 'GET') + uri = '/IPTrack/{0}/{1}/{2}/'.format(self.zone, self.fqdn, service_id) + response = DynectSession.get(uri) self._build(response['data']) def _build(self, data): diff --git a/dyn/tm/services/rttm.py b/dyn/tm/services/rttm.py index 0812eeb..46aac4e 100644 --- a/dyn/tm/services/rttm.py +++ b/dyn/tm/services/rttm.py @@ -74,7 +74,7 @@ def uri(self): def _get(self): args = {'detail': 'Y'} - response = DynectSession.get_session().execute(self.uri, 'GET', args) + response = DynectSession.get(self.uri, args) self._build(response['data']) def to_json(self): @@ -168,13 +168,12 @@ def _post(self): api_args['failover_mode'] = self.failover_mode if self.failover_data: api_args['failover_data'] = self.failover_data - response = DynectSession.get_session().execute(uri, 'POST', api_args) + response = DynectSession.post(uri, api_args) self._build(response['data']) def _update(self, api_args): """Private Update method to cut back on redundant code""" - response = DynectSession.get_session().execute(self.uri, 'PUT', - api_args) + response = DynectSession.put(self.uri, api_args) self._build(response['data']) def _build(self, data): @@ -329,7 +328,7 @@ def _post(self, contact_nickname, performance_monitor, region, ttl=None, api_args['notify_events'] = ', '.join(notify_events) api_args = {api_args[k] for k in api_args if api_args[k] is not None} - resp = DynectSession.get_session().execute(self.uri, 'POST', api_args) + resp = DynectSession.post(self.uri, api_args) self._build(resp['data']) def _build(self, data): @@ -386,8 +385,7 @@ def get_rrset_report(self, ts): :return: dictionary containing rrset report data """ api_args = {'zone': self.zone, 'fqdn': self.fqdn, 'ts': ts} - response = DynectSession.get_session().execute('/RTTMRRSetReport/', - 'POST', api_args) + response = DynectSession.post('/RTTMRRSetReport/', api_args) return response['data'] def get_log_report(self, start_ts, end_ts=None): @@ -404,8 +402,7 @@ def get_log_report(self, start_ts, end_ts=None): api_args = {'zone': self.zone, 'fqdn': self.fqdn, 'start_ts': unix_date(start_ts), 'end_ts': unix_date(end_ts)} - response = DynectSession.get_session().execute('/RTTMLogReport/', - 'POST', api_args) + response = DynectSession.post('/RTTMLogReport/', api_args) return response['data'] def recover(self, recoverip=None, address=None): @@ -415,7 +412,7 @@ def recover(self, recoverip=None, address=None): if recoverip: api_args['recoverip'] = recoverip api_args['address'] = address - resp = DynectSession.get_session().execute(self.uri, 'PUT', api_args) + resp = DynectSession.put(self.uri, api_args) self._build(resp['data']) def __str__(self): diff --git a/dyn/tm/zones.py b/dyn/tm/zones.py index 8a3d38a..b681c9e 100644 --- a/dyn/tm/zones.py +++ b/dyn/tm/zones.py @@ -9,7 +9,7 @@ from .records import RECORD_TYPES from .session import DynectSession from .services import (ActiveFailover, DynamicDNS, DNSSEC, TrafficDirector, - GSLB, ReverseDNS, RTTM) + GSLB, ReverseDNS, RTTM, HTTPRedirect) from ..core import (APIObject, IntegerAttribute, StringAttribute, ListAttribute, ImmutableAttribute, ValidatedAttribute) from ..compat import force_unicode @@ -26,7 +26,7 @@ def get_all_zones(): """ uri = '/Zone/' api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) zones = [] for zone in response['data']: zones.append(Zone(zone['zone'], api=False, **zone)) @@ -41,7 +41,7 @@ def get_all_secondary_zones(): """ uri = '/Secondary/' api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) zones = [] for zone in response['data']: zones.append(SecondaryZone(zone.pop('zone'), api=False, **zone)) @@ -122,8 +122,7 @@ def _post(self, contact=None, ttl=60, serial_style='increment', api_args = {'zone': self.name, 'rname': contact, 'ttl': ttl, 'serial_style': serial_style} - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) + response = DynectSession.post(self.uri, api_args) self._build(response['data']) self._status = 'active' @@ -144,7 +143,7 @@ def _post_with_file(self, file_name): content = f.read() f.close() api_args = {'file': content} - DynectSession.get_session().execute(uri, 'POST', api_args) + DynectSession.post(uri, api_args) self.__poll_for_get() def _xfer(self, master_ip, timeout=None): @@ -153,11 +152,11 @@ def _xfer(self, master_ip, timeout=None): """ uri = '/ZoneTransfer/{0}/'.format(self.name) api_args = {'master_ip': master_ip} - DynectSession.get_session().execute(uri, 'POST', api_args) + DynectSession.post(uri, api_args) time_out = timeout or 10 count = 0 while count < time_out: - response = DynectSession.get_session().execute(uri, 'GET') + response = DynectSession.get(uri) if response['status'] == 'running' and response['message'] == '': sleep(60) count += 1 @@ -185,8 +184,7 @@ def __poll_for_get(self, n_loops=10, xfer=False, xfer_master_ip=None): api_args = {} if xfer_master_ip is not None: api_args['master_ip'] = xfer_master_ip - response = DynectSession.get_session().execute(uri, 'GET', - api_args) + response = DynectSession.get(uri, api_args) error_labels = ['running', 'waiting', 'failed', 'canceled'] ok_labels = ['ready', 'unpublished', 'ok'] if response['data']['status'] in error_labels: @@ -256,7 +254,7 @@ def get_notes(self, offset=None, limit=None): api_args['offset'] = offset if limit: api_args['limit'] = limit - response = DynectSession.get_session().execute(uri, 'POST', api_args) + response = DynectSession.post(uri, api_args) return response['data'] def add_record(self, name=None, record_type='A', *args, **kwargs): @@ -343,7 +341,7 @@ def get_all_records(self): if self.fqdn is not None: uri += '{0}/'.format(self.fqdn) api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) # Strip out empty record_type lists record_lists = {label: rec_list for label, rec_list in response['data'].items() if rec_list != []} @@ -386,7 +384,7 @@ def get_all_records_by_type(self, record_type): constructor = RECORD_TYPES[record_type] uri = '/{0}/{1}/{2}/'.format(names[record_type], self.name, self.fqdn) api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) records = [] for record in response['data']: del record['fqdn'] @@ -407,7 +405,7 @@ def get_any_records(self): return api_args = {'detail': 'Y'} uri = '/ANYRecord/{0}/{1}/'.format(self.name, self.fqdn) - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) # Strip out empty record_type lists record_lists = {label: rec_list for label, rec_list in response['data'].items() if rec_list != []} @@ -435,7 +433,7 @@ def get_all_active_failovers(self): """ uri = '/Failover/{0}/'.format(self.name) api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) afos = [] for failover in response['data']: del failover['zone'] @@ -452,7 +450,7 @@ def get_all_ddns(self): """ uri = '/DDNS/{0}/'.format(self.name) api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) ddnses = [] for ddns in response['data']: del ddns['zone'] @@ -467,9 +465,9 @@ def get_all_httpredirect(self): :return: A :class:`List` of :class:`HTTPRedirect` Services """ - uri = '/HTTPRedirect/{}/'.format(self._name) + uri = '/HTTPRedirect/{0}/'.format(self._name) api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) httpredirs = [] for httpredir in response['data']: del httpredir['zone'] @@ -485,7 +483,7 @@ def get_all_gslb(self): """ uri = '/GSLB/{0}/'.format(self.name) api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) gslbs = [] for gslb_svc in response['data']: del gslb_svc['zone'] @@ -501,7 +499,7 @@ def get_all_rdns(self): """ uri = '/IPTrack/{0}/'.format(self.name) api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) rdnses = [] for rdns in response['data']: del rdns['zone'] @@ -518,7 +516,7 @@ def get_all_rttm(self): """ uri = '/RTTM/{0}/'.format(self.name) api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) rttms = [] for rttm_svc in response['data']: del rttm_svc['zone'] @@ -551,8 +549,7 @@ def get_qps(self, start_ts, end_ts=None, breakdown=None, hosts=None, api_args['hosts'] = hosts if rrecs is not None: api_args['rrecs'] = rrecs - response = DynectSession.get_session().execute('/QPSReport/', - 'POST', api_args) + response = DynectSession.post('/QPSReport/', api_args) return response['data'] def __str__(self): @@ -609,8 +606,7 @@ def _post(self, masters, contact_nickname=None, tsig_key_name=None): api_args['contact_nickname'] = self._contact_nickname if tsig_key_name: api_args['tsig_key_name'] = self._tsig_key_name - response = DynectSession.get_session().execute(self.uri, 'POST', - api_args) + response = DynectSession.post(self.uri, api_args) self._build(response['data']) def activate(self): @@ -703,7 +699,7 @@ def get_all_records(self): if self.fqdn is not None: uri += '{0}/'.format(self.fqdn) api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) # Strip out empty record_type lists record_lists = {label: rec_list for label, rec_list in response['data'].items() if rec_list != []} @@ -746,7 +742,7 @@ def get_all_records_by_type(self, record_type): constructor = RECORD_TYPES[record_type] uri = '/{0}/{1}/{2}/'.format(names[record_type], self.zone, self.fqdn) api_args = {'detail': 'Y'} - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) records = [] for record in response['data']: del record['fqdn'] @@ -764,7 +760,7 @@ def get_any_records(self): return api_args = {'detail': 'Y'} uri = '/ANYRecord/{0}/{1}/'.format(self.zone, self.fqdn) - response = DynectSession.get_session().execute(uri, 'GET', api_args) + response = DynectSession.get(uri, api_args) # Strip out empty record_type lists record_lists = {label: rec_list for label, rec_list in response['data'].items() if rec_list != []} @@ -789,7 +785,7 @@ def delete(self): underneath this node """ uri = '/Node/{0}/{1}'.format(self.zone, self.fqdn) - DynectSession.get_session().execute(uri, 'DELETE') + DynectSession.delete(uri) def __str__(self): return force_unicode(': {0}').format(self.fqdn) From 60f49695c85a5da5c5e1efb03820bf2e2d14d034 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 7 Mar 2015 13:53:37 -0500 Subject: [PATCH 52/59] APIObject => DNSAPIObject to cut back on boilerplate --- dyn/tm/records.py | 10 +++----- dyn/tm/services/_shared.py | 8 +++--- dyn/tm/services/active_failover.py | 5 ++-- dyn/tm/services/ddns.py | 5 ++-- dyn/tm/services/dnssec.py | 39 +++--------------------------- dyn/tm/services/dsf.py | 30 +++++++++-------------- dyn/tm/services/gslb.py | 19 ++++++--------- dyn/tm/services/reversedns.py | 37 +++------------------------- dyn/tm/services/rttm.py | 18 ++++++-------- dyn/tm/zones.py | 14 ++++------- 10 files changed, 50 insertions(+), 135 deletions(-) diff --git a/dyn/tm/records.py b/dyn/tm/records.py index 70c87f3..3df55e8 100644 --- a/dyn/tm/records.py +++ b/dyn/tm/records.py @@ -4,9 +4,9 @@ These DNS_Records should really only need to be created via a zone instance but could also be created independently if passed valid zone, fqdn data """ -from .session import DynectSession -from ..core import (APIObject, ImmutableAttribute, IntegerAttribute, - StringAttribute, ValidatedAttribute) +from .session import DynectSession, DNSAPIObject +from ..core import (ImmutableAttribute, IntegerAttribute, StringAttribute, + ValidatedAttribute) from ..compat import force_unicode __author__ = 'jnappi' @@ -19,12 +19,10 @@ # noinspection PyMissingConstructor -class DNSRecord(APIObject): +class DNSRecord(DNSAPIObject): """Base record object contains functionality to be used across all other record type objects """ - session_type = DynectSession - #: Name of zone that this record belongs to zone = ImmutableAttribute('zone') diff --git a/dyn/tm/services/_shared.py b/dyn/tm/services/_shared.py index 5067fb9..1a8fb7a 100644 --- a/dyn/tm/services/_shared.py +++ b/dyn/tm/services/_shared.py @@ -1,15 +1,13 @@ # -*- coding: utf-8 -*- -from ..session import DynectSession -from ...core import (APIObject, StringAttribute, ValidatedAttribute, - IntegerAttribute) +from ..session import DynectSession, DNSAPIObject +from ...core import StringAttribute, ValidatedAttribute, IntegerAttribute from ...compat import force_unicode __author__ = 'jnappi' -class BaseMonitor(APIObject): +class BaseMonitor(DNSAPIObject): """A :class:`Monitor` for a GSLB Service""" - session_type = DynectSession protocol = ValidatedAttribute('protocol', validator=('HTTP', 'HTTPS', 'PING', 'SMTP', 'TCP')) diff --git a/dyn/tm/services/active_failover.py b/dyn/tm/services/active_failover.py index 34d0d2b..3acf716 100644 --- a/dyn/tm/services/active_failover.py +++ b/dyn/tm/services/active_failover.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from ._shared import BaseMonitor from ..utils import Active, APIList -from ..session import DynectSession +from ..session import DynectSession, DNSAPIObject from ...core import (APIService, ImmutableAttribute, StringAttribute, ClassAttribute, IntegerAttribute, ValidatedListAttribute) from ...compat import force_unicode @@ -20,13 +20,12 @@ def uri(self): raise ValueError -class ActiveFailover(APIService): +class ActiveFailover(APIService, DNSAPIObject): """With Active Failover, we monitor your Primary IP. If a failover event is detected, our system auto switches (hot swaps) to your dedicated back-up IP """ uri = '/Failover/{zone}/{fqdn}/' - session_type = DynectSession #: The zone that this :class:`ActiveFailover` service is attached to zone = ImmutableAttribute('zone') diff --git a/dyn/tm/services/ddns.py b/dyn/tm/services/ddns.py index 4028eec..a64def3 100644 --- a/dyn/tm/services/ddns.py +++ b/dyn/tm/services/ddns.py @@ -2,7 +2,7 @@ """This module contains API Wrapper implementations of the Dynamic DNS service """ from ..utils import Active -from ..session import DynectSession +from ..session import DynectSession, DNSAPIObject from ..accounts import User from ...core import APIService, ImmutableAttribute, StringAttribute from ...compat import force_unicode @@ -11,12 +11,11 @@ __all__ = ['DynamicDNS'] -class DynamicDNS(APIService): +class DynamicDNS(APIService, DNSAPIObject): """DynamicDNS is a service which aliases a dynamic IP Address to a static hostname """ uri = '/DDNS/{zone}/{fqdn}/{rr_type}/' - session_type = DynectSession #: The zone to attach this :class:`DynamicDNS` Service to zone = ImmutableAttribute('zone') diff --git a/dyn/tm/services/dnssec.py b/dyn/tm/services/dnssec.py index b44dc3c..5cf4475 100644 --- a/dyn/tm/services/dnssec.py +++ b/dyn/tm/services/dnssec.py @@ -2,9 +2,9 @@ from datetime import datetime from ..utils import APIList, Active, unix_date -from ..session import DynectSession -from ...core import (APIObject, ImmutableAttribute, StringAttribute, - ValidatedListAttribute) +from ..session import DynectSession, DNSAPIObject +from ...core import (ImmutableAttribute, StringAttribute, + ValidatedListAttribute, APIService) from ...compat import force_unicode __author__ = 'jnappi' @@ -83,10 +83,9 @@ def __str__(self): return force_unicode(': {0}').format(self.algorithm) -class DNSSEC(APIObject): +class DNSSEC(APIService, DNSAPIObject): """A DynECT System DNSSEC Service""" uri = '/DNSSEC/{zone_name}/' - session_type = DynectSession zone = ImmutableAttribute('zone') contact_nickname = StringAttribute('contact_nickname') notify_events = ValidatedListAttribute('notify_events', @@ -149,28 +148,6 @@ def _update(self, **api_args): api_args['notify_events'] = ', '.join(api_args['notify_events']) super(DNSSEC, self)._update(**api_args) - @property - def active(self): - """The current status of this :class:`DNSSEC` service. When setting - directly, rather than using activate/deactivate valid arguments are 'Y' - or True to activate, or 'N' or False to deactivate. Note: If your - service is already active and you try to activate it, nothing will - happen. And vice versa for deactivation. - - :returns: An :class:`Active` object representing the current state of - this :class:`DNSSEC` Service - """ - self._get() # Do a get to ensure we have the most up-to-date status - return self._active - @active.setter - def active(self, value): - deactivate = ('N', False) - activate = ('Y', True) - if value in deactivate and self.active: - self.deactivate() - elif value in activate and not self.active: - self.activate() - @property def keys(self): """A List of :class:`DNSSECKey`'s associated with this :class:`DNSSEC` @@ -188,14 +165,6 @@ def keys(self, value): elif isinstance(value, APIList): self._keys = value - def activate(self): - """Activate this :class:`DNSSEC` service""" - self._update(activate='Y') - - def deactivate(self): - """Deactivate this :class:`DNSSEC` service""" - self._update(deactivate='Y') - def timeline_report(self, start_ts=None, end_ts=None): """Generates a report of events this :class:`DNSSEC` service has performed and has scheduled to perform diff --git a/dyn/tm/services/dsf.py b/dyn/tm/services/dsf.py index 075e1bd..ede408b 100644 --- a/dyn/tm/services/dsf.py +++ b/dyn/tm/services/dsf.py @@ -5,10 +5,10 @@ from ..utils import APIList, Active from ..errors import DynectInvalidArgumentError from ..records import * -from ..session import DynectSession -from ...core import (APIObject, ImmutableAttribute, StringAttribute, - IntegerAttribute, ValidatedAttribute, APIDescriptor, - BooleanAttribute, ListAttribute) +from ..session import DynectSession, DNSAPIObject +from ...core import (ImmutableAttribute, StringAttribute, IntegerAttribute, + ValidatedAttribute, APIDescriptor, BooleanAttribute, + ListAttribute) from ...compat import force_unicode __author__ = 'jnappi' @@ -45,10 +45,9 @@ def get_all_dsf_monitors(): return mons -class _DSFRecord(APIObject): +class _DSFRecord(DNSAPIObject): """Base type for all DSF Records""" uri = '' - session_type = DynectSession label = StringAttribute('label') weight = IntegerAttribute('weight') automation = ValidatedAttribute('automation', @@ -831,11 +830,10 @@ def __init__(self, txtdata, ttl=0, label=None, weight=1, automation='auto', endpoint_up_count, eligible, **kwargs) -class DSFRecordSet(APIObject): +class DSFRecordSet(DNSAPIObject): """A Collection of DSFRecord Type objects belonging to a :class:`DSFFailoverChain` """ - session_type = DynectSession records = ListAttribute('records') status = ImmutableAttribute('status') @@ -1013,9 +1011,8 @@ def __str__(self): return ': {1}'.format(self.rdata_class, self.label) -class DSFFailoverChain(APIObject): +class DSFFailoverChain(DNSAPIObject): uri = '/DSFRecordSetFailoverChain/{0}/{1}/' - session_type = DynectSession #: A unique label for this DSF Failover Chain label = StringAttribute('label') @@ -1121,9 +1118,8 @@ def __str__(self): return ': {0}'.format(self.label) -class DSFResponsePool(APIObject): +class DSFResponsePool(DNSAPIObject): uri = '/DSFResponsePool/{0}/{1}/' - session_type = DynectSession _get_length = 1 #: A unique label for this DSF Response Pool @@ -1245,9 +1241,7 @@ def __str__(self): return ': {0}'.format(self.dsf_response_pool_id) -class DSFRuleset(APIObject): - session_type = DynectSession - +class DSFRuleset(DNSAPIObject): #: A unique label for this DSF Ruleset label = StringAttribute('label') @@ -1430,10 +1424,9 @@ def site_prefs(self, value): self._update(api_args) -class DSFMonitor(APIObject): +class DSFMonitor(DNSAPIObject): """A Monitor for a :class:`TrafficDirector` Service""" uri = '/DSFMonitor/' - session_type = DynectSession _get_length = 1 #: The unique DynECT system id for this DSF Monitor @@ -1527,13 +1520,12 @@ def __str__(self): return ': {0}'.format(self.dsf_monitor_id) -class TrafficDirector(APIObject): +class TrafficDirector(DNSAPIObject): """Traffic Director is a DNS based traffic routing and load balancing service that is Geolocation aware and can support failover by monitoring endpoints. """ uri = '/DSF/' - session_type = DynectSession _get_length = 1 #: The unique DynECT system id for this Traffic Director service diff --git a/dyn/tm/services/gslb.py b/dyn/tm/services/gslb.py index 868363e..966f677 100644 --- a/dyn/tm/services/gslb.py +++ b/dyn/tm/services/gslb.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from ._shared import BaseMonitor from ..utils import APIList -from ..session import DynectSession -from ...core import (APIObject, APIService, ImmutableAttribute, - StringAttribute, ValidatedAttribute, IntegerAttribute, - ClassAttribute, ListAttribute) +from ..session import DynectSession, DNSAPIObject +from ...core import (APIService, ImmutableAttribute, StringAttribute, + ValidatedAttribute, IntegerAttribute, ClassAttribute, + ListAttribute) from ...compat import force_unicode __author__ = 'jnappi' @@ -21,9 +21,8 @@ def uri(self): raise ValueError -class GSLBRegionPoolEntry(APIObject): +class GSLBRegionPoolEntry(DNSAPIObject): uri = '/GSLBRegionPoolEntry/{zone}/{fqdn}/{region}/{address}/' - session_type = DynectSession zone = ImmutableAttribute('zone') fqdn = ImmutableAttribute('fqdn') region_code = ImmutableAttribute('region_code') @@ -86,9 +85,8 @@ def __str__(self): ) -class GSLBRegion(APIObject): +class GSLBRegion(DNSAPIObject): uri = '/GSLBRegion/{zone}/{fqdn}/{region}/' - session_type = DynectSession zone = ImmutableAttribute('zone') fqdn = ImmutableAttribute('fqdn') region_code = ImmutableAttribute('region_code') @@ -182,10 +180,9 @@ def __str__(self): return force_unicode(': {0}').format(self.region_code) -class GSLB(APIService): +class GSLB(APIService, DNSAPIObject): """A Global Server Load Balancing (GSLB) service""" uri = '/GSLB/{zone}/{fqdn}/' - session_type = DynectSession zone = ImmutableAttribute('zone') fqdn = ImmutableAttribute('fqdn') auto_recover = ValidatedAttribute('auto_recover', validator=('Y', 'N')) @@ -293,7 +290,7 @@ def _update(self, **api_args): self._region.uri = self.uri if 'monitor' in api_args: monitor = api_args.pop('monitor') - # We're only going accept new monitors of type Monitor + # We're only going accept new monitors of type Monitor if isinstance(monitor, GSLBMonitor): api_args['monitor'] = monitor.to_json() super(GSLB, self)._update(**api_args) diff --git a/dyn/tm/services/reversedns.py b/dyn/tm/services/reversedns.py index e1243f3..76f29c8 100644 --- a/dyn/tm/services/reversedns.py +++ b/dyn/tm/services/reversedns.py @@ -1,19 +1,18 @@ # -*- coding: utf-8 -*- from ..utils import Active -from ..session import DynectSession -from ...core import (APIObject, StringAttribute, ImmutableAttribute, - ValidatedListAttribute, ListAttribute, IntegerAttribute) +from ..session import DynectSession, DNSAPIObject +from ...core import (APIService, StringAttribute, ImmutableAttribute, + ListAttribute, ValidatedListAttribute, IntegerAttribute) from ...compat import force_unicode __author__ = 'jnappi' __all__ = ['ReverseDNS'] -class ReverseDNS(APIObject): +class ReverseDNS(APIService, DNSAPIObject): """A DynECT ReverseDNS service""" uri = '/IPTrack/{zone}/{fqdn}/' _get_length = 1 - session_type = DynectSession zone = ImmutableAttribute('zone') fqdn = ImmutableAttribute('fqdn') @@ -76,33 +75,5 @@ def _update(self, **api_args): break # Only need to check/assign them once super(ReverseDNS, self)._update(**api_args) - @property - def active(self): - """Indicates whether or not the service is active. When setting - directly, rather than using activate/deactivate valid arguments are 'Y' - or True to activate, or 'N' or False to deactivate. Note: If your - service is already active and you try to activate it, nothing will - happen. And vice versa for deactivation. - - :returns: An :class:`Active` object representing the current state of - this :class:`ReverseDNS` Service - """ - return self._active - @active.setter - def active(self, value): - deactivate, activate = ('N', False), ('Y', True) - if value in deactivate and self.active: - self.deactivate() - elif value in activate and not self.active: - self.activate() - - def activate(self): - """Activate this ReverseDNS service""" - self._update(activate=True) - - def deactivate(self): - """Deactivate this ReverseDNS service""" - self._update(deactivate=True) - def __str__(self): return force_unicode(': {0}').format(self.fqdn) diff --git a/dyn/tm/services/rttm.py b/dyn/tm/services/rttm.py index 46aac4e..4682b18 100644 --- a/dyn/tm/services/rttm.py +++ b/dyn/tm/services/rttm.py @@ -3,10 +3,10 @@ from ._shared import BaseMonitor from ..utils import APIList, Active, unix_date -from ..session import DynectSession -from ...core import (APIObject, APIService, ImmutableAttribute, - StringAttribute, ValidatedAttribute, IntegerAttribute, - ClassAttribute, ValidatedListAttribute, ListAttribute) +from ..session import DynectSession, DNSAPIObject +from ...core import (APIService, ImmutableAttribute, StringAttribute, + ValidatedAttribute, IntegerAttribute, ClassAttribute, + ValidatedListAttribute, ListAttribute) from ...compat import force_unicode __author__ = 'jnappi' @@ -34,12 +34,10 @@ def _build(self, data): super(BaseMonitor, self)._build(data.pop('performance_monitor')) -class RegionPoolEntry(APIObject): +class RegionPoolEntry(DNSAPIObject): """Creates a new RTTM service region pool entry in the zone/node indicated """ - session_type = DynectSession - zone = ImmutableAttribute('zone') fqdn = ImmutableAttribute('fqdn') region_code = ImmutableAttribute('region_code') @@ -87,10 +85,9 @@ def __str__(self): return force_unicode(': {0}').format(self.address) -class RTTMRegion(APIObject): +class RTTMRegion(DNSAPIObject): """docstring for RTTMRegion""" uri = '/RTTMRegion/{zone}/{fqdn}/{region}/' - session_type = DynectSession zone = ImmutableAttribute('zone') fqdn = ImmutableAttribute('fqdn') region_code = ValidatedAttribute('region_code', @@ -204,7 +201,7 @@ def __str__(self): return force_unicode(': {0}').format(self.region_code) -class RTTM(APIService): +class RTTM(APIService, DNSAPIObject): """Real Time Traffic Management (RTTM) is a DynECT Managed DNS service, which monitors all of your endpoints to detect the best-performing ones and also auto-populates your regional pools using that information to provide @@ -214,7 +211,6 @@ class RTTM(APIService): configurations. """ uri = '/RTTM/{zone}/{fqdn}/' - session_type = DynectSession #: The zone that this :class:`RTTM` service is attached to zone = ImmutableAttribute('zone') diff --git a/dyn/tm/zones.py b/dyn/tm/zones.py index b681c9e..e6f5e71 100644 --- a/dyn/tm/zones.py +++ b/dyn/tm/zones.py @@ -7,11 +7,11 @@ from .utils import unix_date from .errors import DynectCreateError, DynectGetError from .records import RECORD_TYPES -from .session import DynectSession +from .session import DynectSession, DNSAPIObject from .services import (ActiveFailover, DynamicDNS, DNSSEC, TrafficDirector, GSLB, ReverseDNS, RTTM, HTTPRedirect) -from ..core import (APIObject, IntegerAttribute, StringAttribute, - ListAttribute, ImmutableAttribute, ValidatedAttribute) +from ..core import (IntegerAttribute, StringAttribute, ListAttribute, + ImmutableAttribute, ValidatedAttribute) from ..compat import force_unicode __author__ = 'jnappi' @@ -49,13 +49,11 @@ def get_all_secondary_zones(): # noinspection PyUnresolvedReferences -class Zone(APIObject): +class Zone(DNSAPIObject): """A class representing a DynECT Primary Zone""" #: Primary Zone URI uri = '/Zone/{zone_name}/' - session_type = DynectSession - #: The name of this Zone zone = ImmutableAttribute('zone') @@ -556,13 +554,11 @@ def __str__(self): return force_unicode(': {0}').format(self.name) -class SecondaryZone(APIObject): +class SecondaryZone(DNSAPIObject): """A class representing DynECT Secondary zones""" # Secondary Zone URI uri = '/Secondary/{zone_name}/' - session_type = DynectSession - #: The name of this secondary zone zone = StringAttribute('zone') From 77a9f013641c23e874e34317cfaeb5752f9562fd Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 7 Mar 2015 14:05:15 -0500 Subject: [PATCH 53/59] Added exception to get_session if there is no current session --- dyn/core.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/dyn/core.py b/dyn/core.py index 344022e..843b03b 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -299,6 +299,15 @@ def append(self, p_object): super(_History, self).append(tuple([now_ts] + list(p_object))) +class NoActiveSessionError(Exception): + """Custom exception type to be raised if `SessionEngine.get_session` is + called and there is not an active session instance for it to return + """ + def __init__(self, sessiontype): + self.message = ('You must have an active {0} instance to perform this ' + 'operation'.format(str(sessiontype))) + + # noinspection PyMethodParameters class SessionEngine(Singleton): """Base object representing a DynectSession Session""" @@ -349,10 +358,16 @@ def new_session(cls, *args, **kwargs): def get_session(cls): """Return the current session for this Session type or None if there is not an active session + + :raises :class:`NoActiveSessionError` if there is not currently an + active session """ cur_thread = threading.current_thread() key = getattr(cls, '__metakey__') - return cls._instances.get(key, {}).get(cur_thread, None) + instance = cls._instances.get(key, {}).get(cur_thread, None) + if instance is None: + raise NoActiveSessionError(cls) + return instance @classmethod def get(cls, url, api_args=None): From 5c10ac26bb901af58a09753e63874830101d8b0a Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 7 Mar 2015 15:22:30 -0500 Subject: [PATCH 54/59] Fixes for Traffic Director --- dyn/tm/services/dsf.py | 115 ++++++++++++++++++++++++++++++++--------- 1 file changed, 92 insertions(+), 23 deletions(-) diff --git a/dyn/tm/services/dsf.py b/dyn/tm/services/dsf.py index ede408b..eaa2525 100644 --- a/dyn/tm/services/dsf.py +++ b/dyn/tm/services/dsf.py @@ -75,8 +75,10 @@ def __init__(self, label=None, weight=1, automation='auto', endpoints=None, """ if 'api' not in kwargs: kwargs['api'] = False - super(_DSFRecord, self).__init__(label, weight, automation, endpoints, - endpoint_up_count, eligible, **kwargs) + DNSAPIObject.__init__(self, label=label, weight=weight, + automation=automation, endpoints=endpoints, + endpoint_up_count=endpoint_up_count, + eligible=eligible, **kwargs) def _post(self, dsf_id, record_set_id): """Create a new :class:`DSFRecord` on the DynECT System @@ -126,7 +128,8 @@ def to_json(self): 'eligible': self.eligible, 'endpoint_up_count': self.endpoint_up_count} json_blob = {x: json[x] for x in json if json[x] is not None} - if hasattr(self, '_record_type') and hasattr(self, 'rdata'): + + if hasattr(self, 'rdata'): # We don't need to worry about rdata() throwing an error since if # we have a record type, then we know we're a subclass of a # DNSRecord @@ -150,6 +153,7 @@ class DSFARecord(_DSFRecord, ARecord): """An :class:`ARecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, address, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -176,6 +180,7 @@ class DSFAAAARecord(_DSFRecord, AAAARecord): """An :class:`AAAARecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, address, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -203,6 +208,7 @@ class DSFCERTRecord(_DSFRecord, CERTRecord): """An :class:`CERTRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, format, tag, algorithm, certificate, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -235,6 +241,7 @@ class DSFCNAMERecord(_DSFRecord, CNAMERecord): """An :class:`CNAMERecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, cname, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -262,6 +269,7 @@ class DSFDHCIDRecord(_DSFRecord, DHCIDRecord): """An :class:`DHCIDRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, digest, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -289,6 +297,7 @@ class DSFDNAMERecord(_DSFRecord, DNAMERecord): """An :class:`DNAMERecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, dname, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -316,6 +325,7 @@ class DSFDNSKEYRecord(_DSFRecord, DNSKEYRecord): """An :class:`DNSKEYRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, protocol, public_key, algorithm=5, flags=256, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -350,6 +360,7 @@ class DSFDSRecord(_DSFRecord, DSRecord): """An :class:`DSRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, digest, keytag, algorithm=5, digtype=1, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -387,6 +398,7 @@ class DSFKEYRecord(_DSFRecord, KEYRecord): """An :class:`KEYRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, algorithm, flags, protocol, public_key, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -421,6 +433,7 @@ class DSFKXRecord(_DSFRecord, KXRecord): """An :class:`KXRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, exchange, preference, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -453,6 +466,7 @@ class DSFLOCRecord(_DSFRecord, LOCRecord): """An :class:`LOCRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, altitude, latitude, longitude, horiz_pre=10000, size=1, vert_pre=10, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, @@ -491,6 +505,7 @@ class DSFIPSECKEYRecord(_DSFRecord, IPSECKEYRecord): """An :class:`IPSECKEYRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, precedence, gatetype, algorithm, gateway, public_key, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, @@ -529,6 +544,7 @@ class DSFMXRecord(_DSFRecord, MXRecord): """An :class:`MXRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, exchange, preference=10, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -560,6 +576,7 @@ class DSFNAPTRRecord(_DSFRecord, NAPTRRecord): """An :class:`NAPTRRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, order, preference, services, regexp, replacement, flags='U', ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, @@ -601,6 +618,7 @@ class DSFPTRRecord(_DSFRecord, PTRRecord): """An :class:`PTRRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, ptrdname, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -628,6 +646,7 @@ class DSFPXRecord(_DSFRecord, PXRecord): """An :class:`PXRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, preference, map822, mapx400, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -659,6 +678,7 @@ class DSFNSAPRecord(_DSFRecord, NSAPRecord): """An :class:`NSAPRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, nsap, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -685,6 +705,7 @@ class DSFRPRecord(_DSFRecord, RPRecord): """An :class:`RPRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ + def __init__(self, mbox, txtdname, ttl=0, label=None, weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): @@ -714,8 +735,9 @@ class DSFNSRecord(_DSFRecord, NSRecord): """An :class:`NSRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ - def __init__(self, nsdname, service_class='', ttl=0, label=None, weight=1, - automation='auto', endpoints=None, endpoint_up_count=None, + + def __init__(self, nsdname, service_class='', ttl=0, label=None, weight=1, + automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): """Create a :class:`DSFNSRecord` object @@ -743,8 +765,9 @@ class DSFSPFRecord(_DSFRecord, SPFRecord): """An :class:`SPFRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ - def __init__(self, txtdata, ttl=0, label=None, weight=1, automation='auto', - endpoints=None, endpoint_up_count=None, eligible=True, + + def __init__(self, txtdata, ttl=0, label=None, weight=1, automation='auto', + endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): """Create a :class:`DSFSPFRecord` object @@ -770,8 +793,9 @@ class DSFSRVRecord(_DSFRecord, SRVRecord): """An :class:`SRVRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ - def __init__(self, port, priority, target, rr_weight, ttl=0, label=None, - weight=1, automation='auto', endpoints=None, + + def __init__(self, port, priority, target, rr_weight, ttl=0, label=None, + weight=1, automation='auto', endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): """Create a :class:`DSFSRVRecord` object @@ -806,8 +830,9 @@ class DSFTXTRecord(_DSFRecord, TXTRecord): """An :class:`TXTRecord` object which is able to store additional data for use by a :class:`TrafficDirector` service. """ - def __init__(self, txtdata, ttl=0, label=None, weight=1, automation='auto', - endpoints=None, endpoint_up_count=None, eligible=True, + + def __init__(self, txtdata, ttl=0, label=None, weight=1, automation='auto', + endpoints=None, endpoint_up_count=None, eligible=True, **kwargs): """Create a :class:`DSFTXTRecord` object @@ -1000,7 +1025,7 @@ def to_json(self): 'eligible': self.eligible, 'dsf_monitor_id': self.dsf_monitor_id, 'records': [rec.to_json() for rec in self.records]} - return {json_blob[key] for key in json_blob if json_blob[key]} + return {key: json_blob[key] for key in json_blob if json_blob[key]} def delete(self): """Delete this :class:`DSFRecordSet` from the Dynect System""" @@ -1154,7 +1179,7 @@ class DSFResponsePool(DNSAPIObject): #: DFS Response Pool belongs to dsf_id = ImmutableAttribute('dsf_id') - def __init__(self, label, dsf_id, core_set_count=1, eligible=True, + def __init__(self, label, dsf_id=None, core_set_count=1, eligible=True, automation='auto', dsf_ruleset_id=None, index=None, rs_chains=None, **kwargs): """Create a :class:`DSFResponsePool` object @@ -1174,6 +1199,8 @@ def __init__(self, label, dsf_id, core_set_count=1, eligible=True, for this :class:`DSFResponsePool` """ self._label, self._dsf_id = label, dsf_id + if dsf_id is None: + kwargs['api'] = False if isinstance(rs_chains, list) and len(rs_chains) > 0 and \ isinstance(rs_chains[0], dict): @@ -1335,6 +1362,7 @@ def __str__(self): class DSFMonitorEndpoint(object): """An Endpoint object to be passed to a :class:`DSFMonitor`""" + def __init__(self, address, label, active='Y', site_prefs=None): """Create a :class:`DSFMonitorEndpoint` object @@ -1365,8 +1393,7 @@ def _update(self, api_args): args_list.append(api_args) else: args_list.append(endpoint.to_json()) - api_args = {'endpoints': args_list} - self._monitor._update(api_args) + self._monitor._update(endpoints=args_list) def to_json(self): """Get the JSON representation of this :class:`DSFMonitorEndpoint` @@ -1387,6 +1414,7 @@ def active(self): this :class:`DSFMonitorEndpoint` """ return self._active + @active.setter def active(self, value): valid_input = ('Y', 'N', True, False) @@ -1399,6 +1427,7 @@ def active(self, value): @property def label(self): return self._label + @label.setter def label(self, value): api_args = self.to_json() @@ -1408,6 +1437,7 @@ def label(self, value): @property def address(self): return self._address + @address.setter def address(self, value): api_args = self.to_json() @@ -1417,6 +1447,7 @@ def address(self, value): @property def site_prefs(self): return self._site_prefs + @site_prefs.setter def site_prefs(self, value): api_args = self.to_json() @@ -1462,6 +1493,42 @@ class DSFMonitor(DNSAPIObject): #: DSFMonitor endpoints = ListAttribute('endpoints') + def __init__(self, label=None, monitor_id=None, protocol=None, + response_count=None, probe_interval=None, retries=None, + active='Y', timeout=None, port=None, path=None, host=None, + header=None, expected=None, endpoints=None): + """ + + :param label: + :param monitor_id: + :param protocol: + :param response_count: + :param probe_interval: + :param retries: + :param active: + :param timeout: + :param port: + :param path: + :param host: + :param header: + :param expected: + :param endpoints: + :return: + """ + if monitor_id is not None: + super(DSFMonitor, self).__init__(monitor_id=monitor_id) + else: + self._monitor_id = None + super(DSFMonitor, self).__init__(label=label, protocol=protocol, + response_count=response_count, + probe_interval=probe_interval, + retries=retries, active=active, + timeout=timeout, port=port, + path=path, + host=host, header=header, + expected=expected, + endpoints=endpoints) + def _get(self, monitor_id): """Get an existing :class:`DSFMonitor` from the DynECT System""" self.uri = '/DSFMonitor/{0}/'.format(monitor_id) @@ -1490,8 +1557,13 @@ def _post(self, label, protocol, response_count, probe_interval, retries, 'probe_interval': probe_interval, 'retries': retries, 'active': str(self._active), 'options': self._options} - if self._endpoints is not None: - api_args['endpoints'] = [x.to_json() for x in self._endpoints] + + if self.endpoints is None and endpoints is not None: + self._endpoints = endpoints + + if self.endpoints is not None: + api_args['endpoints'] = [x.to_json() for x in self.endpoints] + response = DynectSession.post(self.uri, api_args) self._build(response['data']) @@ -1559,6 +1631,9 @@ def __init__(self, *args, **kwargs): self._notifiers.uri = self._nodes.uri = self._rulesets.uri = self.uri super(TrafficDirector, self).__init__(*args, **kwargs) + self.uri = '/DSF/{service_id}'.format(service_id=self.service_id) + self._notifiers.uri = self._nodes.uri = self._rulesets.uri = self.uri + def _post(self, label, ttl=None, publish='Y', nodes=None, notifiers=None, rulesets=None): """Create a new :class:`TrafficDirector` on the DynECT System""" @@ -1605,12 +1680,6 @@ def _get(self, service_id): resp = DynectSession.get(self.uri, api_args) self._build(resp['data']) - def _update(self, **api_args): - """Private update method""" - if 'publish' not in api_args: - api_args['publish'] = 'Y' - super(TrafficDirector, self)._update(**api_args) - def revert_changes(self): """Clears the changeset for this service and reverts all non-published changes to their original state From ebc085b488e4baa2af0f6a074b0db335a19e61fe Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 7 Mar 2015 15:22:46 -0500 Subject: [PATCH 55/59] Adding some smarter TypedAttribute logic --- dyn/core.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dyn/core.py b/dyn/core.py index 843b03b..74b4556 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -99,7 +99,18 @@ class TypedAttribute(APIDescriptor): """Type enforced descriptor""" ty = object + def __get__(self, instance, owner): + """Force our returned value to be returned as our specified type, but + don't choke and die if something goes wrong + """ + calculated = super(TypedAttribute, self).__get__(instance, owner) + try: + return self.ty(calculated) + except (TypeError, ValueError): + return calculated + def __set__(self, instance, value): + """Before overwriting *value*, ensure that it is of the correct type""" if not isinstance(value, self.ty): raise TypeError('Expected %s' % self.ty) super(TypedAttribute, self).__set__(instance, value) From b244b0d248d8597c601191764798319fadea283f Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 7 Mar 2015 15:47:03 -0500 Subject: [PATCH 56/59] Adding the ability to easily wrap services with deprecation warnings --- dyn/core.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dyn/core.py b/dyn/core.py index 74b4556..f635bff 100644 --- a/dyn/core.py +++ b/dyn/core.py @@ -8,14 +8,31 @@ import time import locale import logging +import warnings import threading from datetime import datetime +from functools import wraps from . import __version__ from .compat import (HTTPConnection, HTTPSConnection, HTTPException, json, prepare_to_send, force_unicode, string_types) +def deprecation_warning(f): + """Log a deprecation warning when a decorated method is called. This + decorator is used to encourage users to steer away from currently supported + deprecated services + """ + @wraps(f) + def inner(*args, **kwargs): + msg = ('WARNING: This DynECT service is now deprecated and will be ' + 'removed in a future version of this library. For information ' + 'on upgrading, please visit http://help.dyn.com/') + warnings.warn(msg, DeprecationWarning, stacklevel=2) + return f(*args, **kwargs) + return inner + + def cleared_class_dict(dict_obj): """Return a cleared dict of class attributes. The items cleared are any fields which evaluate to None, and any methods From ddfbf91926e04f8ea5d9dddcc7b1936a26235ef3 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 7 Mar 2015 15:47:30 -0500 Subject: [PATCH 57/59] Simplify dyn.tm.session exports --- dyn/tm/session.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dyn/tm/session.py b/dyn/tm/session.py index 454b6c6..606874a 100644 --- a/dyn/tm/session.py +++ b/dyn/tm/session.py @@ -10,6 +10,8 @@ from ..compat import force_unicode from ..encrypt import AESCipher +__all__ = ['DynectSession', 'DNSAPIObject'] + class DynectSession(SessionEngine): """Base object representing a DynectSession Session""" From cb73d2199631b1dd36405bf3d615d07975bcd8a5 Mon Sep 17 00:00:00 2001 From: Jon Nappi Date: Sat, 7 Mar 2015 15:47:44 -0500 Subject: [PATCH 58/59] 2.0.0 Version bump --- dyn/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dyn/__init__.py b/dyn/__init__.py index b9b148e..534c08e 100644 --- a/dyn/__init__.py +++ b/dyn/__init__.py @@ -5,7 +5,7 @@ Requires Python 2.6 or higher, or the "simplejson" package. """ -version_info = (1, 3, 8) +version_info = (2, 0, 0) __name__ = 'dyn' __doc__ = 'A python wrapper for the DynDNS and DynEmail APIs' __author__ = 'Jonathan Nappi, Cole Tuininga' From 4ebe93f9be95b0ed664b5e74aac502fc63c2eccf Mon Sep 17 00:00:00 2001 From: moogar0880 Date: Sat, 7 Mar 2015 15:55:30 -0500 Subject: [PATCH 59/59] Small user tweak