-
-
Notifications
You must be signed in to change notification settings - Fork 305
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
21 changed files
with
2,055 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
from base import Provider as BaseProvider | ||
import requests | ||
import json | ||
|
||
class Provider(BaseProvider): | ||
|
||
def __init__(self, options, provider_options={}): | ||
super(Provider, self).__init__(options) | ||
self.domain_id = None | ||
self.api_endpoint = provider_options.get('api_endpoint') or 'https://api.nsone.net/v1' | ||
|
||
def authenticate(self): | ||
|
||
payload = self._get('/zones/{0}'.format(self.options['domain'])) | ||
|
||
if not payload['id']: | ||
raise StandardError('No domain found') | ||
|
||
self.domain_id = self.options['domain'] | ||
|
||
|
||
# Create record. If record already exists with the same content, do nothing' | ||
def create_record(self, type, name, content): | ||
record = { | ||
'type': type, | ||
'domain': self._clean_name(name), | ||
'zone': self.domain_id, | ||
'answers':[ | ||
{"answer": [content]} | ||
] | ||
} | ||
payload = {} | ||
try: | ||
payload = self._put('/zones/{0}/{1}/{2}'.format(self.domain_id, self._clean_name(name),type), record) | ||
except requests.exceptions.HTTPError, e: | ||
if e.response.status_code == 400: | ||
payload = {} | ||
|
||
# http 400 is ok here, because the record probably already exists | ||
print 'create_record: {0}'.format('id' in payload) | ||
return 'id' in payload | ||
|
||
# List all records. Return an empty list if no records found | ||
# type, name and content are used to filter records. | ||
# If possible filter during the query, otherwise filter after response is received. | ||
def list_records(self, type=None, name=None, content=None): | ||
filter = {} | ||
|
||
payload = self._get('/zones/{0}'.format(self.domain_id)) | ||
records = [] | ||
for record in payload['records']: | ||
processed_record = { | ||
'type': record['type'], | ||
'name': record['domain'], | ||
'ttl': record['ttl'], | ||
'content': record['short_answers'][0], | ||
#this id is useless unless your doing record linking. Lets return the original record identifier. | ||
'id': '{0}/{1}/{2}'.format(self.domain_id, record['domain'], record['type']) # | ||
} | ||
records.append(processed_record) | ||
|
||
if type: | ||
records = [record for record in records if record['type'] == type] | ||
if name: | ||
records = [record for record in records if record['name'] == self._clean_name(name)] | ||
if content: | ||
records = [record for record in records if record['content'] == content] | ||
|
||
print 'list_records: {0}'.format(records) | ||
return records | ||
|
||
# Create or update a record. | ||
def update_record(self, identifier, type=None, name=None, content=None): | ||
|
||
data = {} | ||
payload = None | ||
new_identifier = "{0}/{1}/{2}".format(self.domain_id, self._clean_name(name),type) | ||
|
||
if(new_identifier == identifier or (type is None and name is None)): | ||
# the identifier hasnt changed, or type and name are both unspecified, only update the content. | ||
data['answers'] = [ | ||
{"answer": [content]} | ||
] | ||
self._post('/zones/{0}'.format(identifier), data) | ||
|
||
else: | ||
# identifiers are different | ||
# get the old record, create a new one with updated data, delete the old record. | ||
old_record = self._get('/zones/{0}'.format(identifier)) | ||
self.create_record(type or old_record['type'], name or old_record['domain'], content or old_record['answers'][0]['answer'][0]) | ||
self.delete_record(identifier) | ||
|
||
print 'update_record: {0}'.format(True) | ||
return True | ||
|
||
# Delete an existing record. | ||
# If record does not exist, do nothing. | ||
def delete_record(self, identifier=None, type=None, name=None, content=None): | ||
if not identifier: | ||
records = self.list_records(type, name, content) | ||
print records | ||
if len(records) == 1: | ||
identifier = records[0]['id'] | ||
else: | ||
raise StandardError('Record identifier could not be found.') | ||
payload = self._delete('/zones/{0}'.format(identifier)) | ||
|
||
# is always True at this point, if a non 200 response is returned an error is raised. | ||
print 'delete_record: {0}'.format(True) | ||
return True | ||
|
||
|
||
# Helpers | ||
|
||
# record names can be in a variety of formats: relative (sub), full (sub.example.com), and fqdn (sub.example.com.) | ||
# NS1 only handles full record names, so we need to make sure we clean up all user specified record_names before | ||
# submitting them to NS1 | ||
def _clean_name(self, record_name): | ||
record_name = record_name.rstrip('.') # strip trailing period from fqdn if present | ||
#check if the record_name is fully specified | ||
if not record_name.endswith(self.options['domain']): | ||
record_name = "{0}.{1}".format(record_name, self.options['domain']) | ||
return record_name | ||
|
||
def _get(self, url='/', query_params={}): | ||
return self._request('GET', url, query_params=query_params) | ||
|
||
def _post(self, url='/', data={}, query_params={}): | ||
return self._request('POST', url, data=data, query_params=query_params) | ||
|
||
def _put(self, url='/', data={}, query_params={}): | ||
return self._request('PUT', url, data=data, query_params=query_params) | ||
|
||
def _delete(self, url='/', query_params={}): | ||
return self._request('DELETE', url, query_params=query_params) | ||
|
||
def _request(self, action='GET', url='/', data={}, query_params={}): | ||
|
||
default_headers = { | ||
'Accept': 'application/json', | ||
'Content-Type': 'application/json', | ||
'X-NSONE-Key': self.options.get('auth_password') or self.options.get('auth_token') | ||
} | ||
default_auth = None | ||
|
||
r = requests.request(action, self.api_endpoint + url, params=query_params, | ||
data=json.dumps(data), | ||
headers=default_headers, | ||
auth=default_auth) | ||
r.raise_for_status() # if the request fails for any reason, throw an error. | ||
return r.json() |
33 changes: 33 additions & 0 deletions
33
tests/fixtures/cassettes/nsone/IntegrationTests/test_Provider_authenticate.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
interactions: | ||
- request: | ||
body: '{}' | ||
headers: | ||
Accept: [application/json] | ||
Accept-Encoding: ['gzip, deflate'] | ||
Connection: [keep-alive] | ||
Content-Length: ['2'] | ||
Content-Type: [application/json] | ||
User-Agent: [python-requests/2.9.1] | ||
method: GET | ||
uri: https://api.nsone.net/v1/zones/capsulecd.com | ||
response: | ||
body: {string: !!python/unicode '{"nx_ttl":3600,"retry":7200,"zone":"capsulecd.com","network_pools":["p06"],"primary":{"enabled":false,"secondaries":[]},"refresh":43200,"expiry":1209600,"dns_servers":["dns1.p06.nsone.net","dns2.p06.nsone.net","dns3.p06.nsone.net","dns4.p06.nsone.net"],"records":[{"domain":"capsulecd.com","short_answers":["dns1.p06.nsone.net","dns2.p06.nsone.net","dns3.p06.nsone.net","dns4.p06.nsone.net"],"ttl":3600,"tier":1,"type":"NS","id":"56f85f4e9f939b00066237b0"}],"meta":{},"link":null,"ttl":3600,"id":"56f85f4e9f939b00066237ab","hostmaster":"[email protected]","networks":[0],"pool":"p06"} | ||
|
||
'} | ||
headers: | ||
cache-control: [no-cache] | ||
connection: [keep-alive] | ||
content-length: ['588'] | ||
content-type: [application/json] | ||
date: ['Sun, 27 Mar 2016 22:43:02 GMT'] | ||
etag: [W/"8c4cce560a475eca3ef1ca2ba80888595d8b9f6c"] | ||
expires: ['0'] | ||
pragma: [no-cache] | ||
server: [NSONE API v1] | ||
transfer-encoding: [chunked] | ||
x-ratelimit-by: [customer] | ||
x-ratelimit-limit: ['900'] | ||
x-ratelimit-period: ['300'] | ||
x-ratelimit-remaining: ['899'] | ||
status: {code: 200, message: OK} | ||
version: 1 |
31 changes: 31 additions & 0 deletions
31
.../nsone/IntegrationTests/test_Provider_authenticate_with_unmanaged_domain_should_fail.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
interactions: | ||
- request: | ||
body: '{}' | ||
headers: | ||
Accept: [application/json] | ||
Accept-Encoding: ['gzip, deflate'] | ||
Connection: [keep-alive] | ||
Content-Length: ['2'] | ||
Content-Type: [application/json] | ||
User-Agent: [python-requests/2.9.1] | ||
method: GET | ||
uri: https://api.nsone.net/v1/zones/thisisadomainidonotown.com | ||
response: | ||
body: {string: !!python/unicode '{"message":"zone not found"} | ||
|
||
'} | ||
headers: | ||
cache-control: [no-cache] | ||
connection: [keep-alive] | ||
content-length: ['29'] | ||
content-type: [application/json] | ||
date: ['Sun, 27 Mar 2016 22:43:02 GMT'] | ||
expires: ['0'] | ||
pragma: [no-cache] | ||
server: [NSONE API v1] | ||
x-ratelimit-by: [customer] | ||
x-ratelimit-limit: ['900'] | ||
x-ratelimit-period: ['300'] | ||
x-ratelimit-remaining: ['899'] | ||
status: {code: 404, message: Not Found} | ||
version: 1 |
63 changes: 63 additions & 0 deletions
63
...tionTests/test_Provider_when_calling_create_record_for_A_with_valid_name_and_content.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
interactions: | ||
- request: | ||
body: '{}' | ||
headers: | ||
Accept: [application/json] | ||
Accept-Encoding: ['gzip, deflate'] | ||
Connection: [keep-alive] | ||
Content-Length: ['2'] | ||
Content-Type: [application/json] | ||
User-Agent: [python-requests/2.9.1] | ||
method: GET | ||
uri: https://api.nsone.net/v1/zones/capsulecd.com | ||
response: | ||
body: {string: !!python/unicode '{"nx_ttl":3600,"retry":7200,"zone":"capsulecd.com","network_pools":["p06"],"primary":{"enabled":false,"secondaries":[]},"refresh":43200,"expiry":1209600,"dns_servers":["dns1.p06.nsone.net","dns2.p06.nsone.net","dns3.p06.nsone.net","dns4.p06.nsone.net"],"records":[{"domain":"capsulecd.com","short_answers":["dns1.p06.nsone.net","dns2.p06.nsone.net","dns3.p06.nsone.net","dns4.p06.nsone.net"],"ttl":3600,"tier":1,"type":"NS","id":"56f85f4e9f939b00066237b0"}],"meta":{},"link":null,"ttl":3600,"id":"56f85f4e9f939b00066237ab","hostmaster":"[email protected]","networks":[0],"pool":"p06"} | ||
|
||
'} | ||
headers: | ||
cache-control: [no-cache] | ||
connection: [keep-alive] | ||
content-length: ['588'] | ||
content-type: [application/json] | ||
date: ['Sun, 27 Mar 2016 22:56:09 GMT'] | ||
etag: [W/"8c4cce560a475eca3ef1ca2ba80888595d8b9f6c"] | ||
expires: ['0'] | ||
pragma: [no-cache] | ||
server: [NSONE API v1] | ||
x-ratelimit-by: [customer] | ||
x-ratelimit-limit: ['900'] | ||
x-ratelimit-period: ['300'] | ||
x-ratelimit-remaining: ['899'] | ||
status: {code: 200, message: OK} | ||
- request: | ||
body: '{"domain": "localhost.capsulecd.com", "type": "A", "answers": [{"answer": | ||
["127.0.0.1"]}], "zone": "capsulecd.com"}' | ||
headers: | ||
Accept: [application/json] | ||
Accept-Encoding: ['gzip, deflate'] | ||
Connection: [keep-alive] | ||
Content-Length: ['115'] | ||
Content-Type: [application/json] | ||
User-Agent: [python-requests/2.9.1] | ||
method: PUT | ||
uri: https://api.nsone.net/v1/zones/capsulecd.com/localhost.capsulecd.com/A | ||
response: | ||
body: {string: !!python/unicode '{"domain":"localhost.capsulecd.com","zone":"capsulecd.com","use_client_subnet":true,"answers":[{"answer":["127.0.0.1"],"id":"56f8650b1c372700067a75ab"}],"id":"56f8650b1c372700067a75ac","regions":{},"meta":{},"link":null,"filters":[],"ttl":3600,"tier":1,"type":"A","networks":[0]} | ||
|
||
'} | ||
headers: | ||
cache-control: [no-cache] | ||
connection: [keep-alive] | ||
content-length: ['280'] | ||
content-type: [application/json] | ||
date: ['Sun, 27 Mar 2016 22:56:11 GMT'] | ||
expires: ['0'] | ||
pragma: [no-cache] | ||
server: [NSONE API v1] | ||
transfer-encoding: [chunked] | ||
x-ratelimit-by: [customer] | ||
x-ratelimit-limit: ['100'] | ||
x-ratelimit-period: ['200'] | ||
x-ratelimit-remaining: ['99'] | ||
status: {code: 200, message: OK} | ||
version: 1 |
Oops, something went wrong.