Skip to content

Commit

Permalink
Merge 8e4a684 into 48485d6
Browse files Browse the repository at this point in the history
  • Loading branch information
AnalogJ committed Mar 28, 2016
2 parents 48485d6 + 8e4a684 commit 2f15b23
Show file tree
Hide file tree
Showing 21 changed files with 2,055 additions and 7 deletions.
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,27 @@ Only DNS providers who have an API can be supported by `lexicon`.

The current supported providers are:

- cloudflare ([docs](https://api.cloudflare.com/#endpoints))
- pointhq ([docs](https://pointhq.com/api/docs))
- dnsimple ([docs](https://developer.dnsimple.com/))
- Cloudflare ([docs](https://api.cloudflare.com/#endpoints))
- PointHQ ([docs](https://pointhq.com/api/docs))
- DNSimple ([docs](https://developer.dnsimple.com/))
- DnsMadeEasy ([docs](http://www.dnsmadeeasy.com/integration/pdf/API-Docv2.pdf))
- NS1 ([docs](https://ns1.com/api/))

The next planned providers are:

- NS1 ([docs](https://ns1.com/api/))
- Rackspace ([docs](https://developer.rackspace.com/docs/cloud-dns/v1/developer-guide/))
- ClouDNS ([docs](https://www.cloudns.net/wiki/article/56/))
- Rage4 ([docs](https://gbshouse.uservoice.com/knowledgebase/articles/109834-rage4-dns-developers-api))
- Namecheap ([docs](https://www.namecheap.com/support/api/methods.aspx))
- AWS Route53 ([docs](https://docs.aws.amazon.com/Route53/latest/APIReference/Welcome.html))
- DnsMadeEasy ([docs](http://www.dnsmadeeasy.com/integration/pdf/API-Docv2.pdf))
- Mythic Beasts([docs](https://www.mythic-beasts.com/support/api/primary))
- PowerDNS ([docs](https://doc.powerdns.com/md/httpapi/api_spec/))
- Google Cloud DNS ([docs](https://cloud.google.com/dns/api/v1/))
- BuddyDNS ([docs](https://www.buddyns.com/support/api/v2/))
- Linode ([docs](https://www.linode.com/api/dns))
- Namesilo ([docs](https://www.namesilo.com/api_reference.php))
- AHNames ([docs](https://ahnames.com/en/resellers?tab=2))
- EntryDNS ([docs](https://entrydns.net/help))

## Setup
To use lexicon as a CLI application, do the following:
Expand All @@ -43,13 +51,13 @@ You can also install the latest version from the repository directly.
[--priority=PRIORITY] [--identifier=IDENTIFIER]
[--auth-username=AUTH_USERNAME] [--auth-password=AUTH_PASSWORD]
[--auth-token=AUTH_TOKEN] [--auth-otp-token=AUTH_OTP_TOKEN]
{base,cloudflare,__init__} {create,list,update,delete} domain
{cloudflare, dnsimple, dnsmadeeasy, nsone, pointhq} {create,list,update,delete} domain
{A,CNAME,MX,SOA,TXT}

Create, Update, Delete, List DNS entries

positional arguments:
{cloudflare}
{cloudflare, dnsimple, dnsmadeeasy, nsone, pointhq}
specify the DNS provider to use
{create,list,update,delete}
specify the action to take
Expand Down Expand Up @@ -107,6 +115,7 @@ There is an included example Dockerfile that can be used to automatically genera
- [x] Create and Register a lexicon pip package.
- [ ] Write documentation on supported environmental variables.
- [ ] Wire up automated release packaging on PRs.
- [ ] Check for additional dns hosts with apis (from [fog](http://fog.io/about/provider_documentation.html))

## Contributing Changes.
If the DNS provider you use is not already available, please consider contributing by opening a pull request.
Expand Down
151 changes: 151 additions & 0 deletions lexicon/providers/nsone.py
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()
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
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
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
Loading

0 comments on commit 2f15b23

Please sign in to comment.