diff --git a/docs/dns/drivers/vultr.rst b/docs/dns/drivers/vultr.rst new file mode 100644 index 0000000000..f74baf8c85 --- /dev/null +++ b/docs/dns/drivers/vultr.rst @@ -0,0 +1,24 @@ +Vultr Driver Documentation +=========================== + +`Vultr`_ was built by the same team that created Choopa.com and +GameServers.com, has tackled hosting solutions, delivering industry +leading performance and reliability while building out one highly available +worldwide network. + +Read more at: https://www.vultr.com/about/ + +Instantiating the driver +-------------------------- + +.. literalinclude:: /examples/dns/vultr/instantiate_driver.py + :language: python + +API Docs +-------- + +.. autoclass:: libcloud.dns.drivers.vultr.VultrDNSDriver + :members: + :inherited-members: + +.. _`Vultr`: https://www.vultr.com diff --git a/docs/examples/dns/vultr/instantiate_driver.py b/docs/examples/dns/vultr/instantiate_driver.py new file mode 100644 index 0000000000..099e2b055c --- /dev/null +++ b/docs/examples/dns/vultr/instantiate_driver.py @@ -0,0 +1,5 @@ +from libcloud.dns.types import Provider +from libcloud.dns.providers import get_driver + +cls = get_driver(Provider.VULTR) +driver = cls(key='api_key') diff --git a/libcloud/common/vultr.py b/libcloud/common/vultr.py new file mode 100644 index 0000000000..37bc76e5d8 --- /dev/null +++ b/libcloud/common/vultr.py @@ -0,0 +1,137 @@ +from libcloud.common.base import ConnectionKey, JsonResponse +from libcloud.utils.misc import lowercase_keys +from libcloud.utils.py3 import PY3, b + + +__all__ = [ + 'API_HOST', + 'VultrConnection', + 'VultrException', + 'VultrResponse', +] + +# Endpoint for the Vultr API +API_HOST = 'api.vultr.com' + + +class VultrResponse(JsonResponse): + + objects = None + error_dict = {} + errors = None + ERROR_CODE_MAP = { + + 400: "Invalid API location. Check the URL that you are using.", + 403: "Invalid or missing API key. Check that your API key is present" + + " and matches your assigned key.", + 405: "Invalid HTTP method. Check that the method (POST|GET) matches" + + " what the documentation indicates.", + 412: "Request failed. Check the response body for a more detailed" + + " description.", + 500: "Internal server error. Try again at a later time.", + 503: "Rate limit hit. API requests are limited to an average of 1/s." + + " Try your request again later.", + + } + + def __init__(self, response, connection): + + self.connection = connection + self.error = response.reason + self.status = response.status + self.headers = lowercase_keys(dict(response.getheaders())) + + original_data = getattr(response, '_original_data', None) + + if original_data: + self.body = response._original_data + + else: + self.body = self._decompress_response(body=response.read(), + headers=self.headers) + + if PY3: + self.body = b(self.body).decode('utf-8') + + self.objects, self.errors = self.parse_body() + if not self.success(): + raise self._make_excp(self.errors[0]) + + def parse_body(self): + """ + Returns JSON data in a python list. + """ + json_objects = [] + errors = [] + + if self.status in self.ERROR_CODE_MAP: + self.error_dict['ERRORCODE'] = self.status + self.error_dict['ERRORMESSAGE'] = self.ERROR_CODE_MAP[self.status] + errors.append(self.error_dict) + + js = super(VultrResponse, self).parse_body() + if isinstance(js, dict): + js = [js] + + json_objects.append(js) + + return (json_objects, errors) + + def _make_excp(self, error): + """ + Convert API error to a VultrException instance + """ + + return VultrException(error['ERRORCODE'], error['ERRORMESSAGE']) + + def success(self): + + return len(self.errors) == 0 + + +class VultrConnection(ConnectionKey): + """ + A connection to the Vultr API + """ + host = API_HOST + responseCls = VultrResponse + + def add_default_params(self, params): + """ + Returns default params such as api_key which is + needed to perform an action.Returns a dictionary. + Example:/v1/server/upgrade_plan?api_key=self.key + """ + params['api_key'] = self.key + + return params + + def add_default_headers(self, headers): + """ + Returns default headers such as content-type. + Returns a dictionary. + """ + headers["Content-Type"] = "application/x-www-form-urlencoded" + headers["Accept"] = "text/plain" + + return headers + + def set_path(self): + self.path = '/v/' + return self.path + + +class VultrException(Exception): + """ + Error originating from the Vultr API + """ + def __init__(self, code, message): + self.code = code + self.message = message + self.args = (code, message) + + def __str__(self): + return "(%u) %s" % (self.code, self.message) + + def __repr__(self): + return "VultrException code %u '%s'" % (self.code, self.message) diff --git a/libcloud/dns/drivers/vultr.py b/libcloud/dns/drivers/vultr.py new file mode 100644 index 0000000000..a22f68fff0 --- /dev/null +++ b/libcloud/dns/drivers/vultr.py @@ -0,0 +1,391 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Vultr DNS Driver +""" + +from libcloud.utils.py3 import urllib +from libcloud.common.vultr import VultrConnection, VultrResponse +from libcloud.dns.base import DNSDriver, Zone, Record +from libcloud.dns.types import ZoneDoesNotExistError, RecordDoesNotExistError +from libcloud.dns.types import ZoneAlreadyExistsError, RecordAlreadyExistsError +from libcloud.dns.types import Provider, RecordType + + +__all__ = [ + 'ZoneRequiredException', + 'VultrDNSResponse', + 'VultrDNSConnection', + 'VultrDNSDriver', +] + + +class ZoneRequiredException(Exception): + pass + + +class VultrDNSResponse(VultrResponse): + pass + + +class VultrDNSConnection(VultrConnection): + responseCls = VultrDNSResponse + + +class VultrDNSDriver(DNSDriver): + + type = Provider.VULTR + name = 'Vultr DNS' + website = 'http://www.vultr.com/' + connectionCls = VultrDNSConnection + + RECORD_TYPE_MAP = { + + RecordType.A: 'A', + RecordType.AAAA: 'AAAA', + RecordType.TXT: 'TXT', + RecordType.CNAME: 'CNAME', + RecordType.MX: 'MX', + RecordType.NS: 'NS', + RecordType.SRV: 'SRV', + } + + def list_zones(self): + """ + Return a list of records for the provided zone. + + :param zone: Zone to list records for. + :type zone: :class:`Zone` + + :return: ``list`` of :class:`Record` + """ + action = '/v1/dns/list' + params = {'api_key': self.key} + response, errors = self.connection.request(action=action, + params=params).parse_body() + zones = self._to_zones(response[0]) + + return zones + + def list_records(self, zone): + """ + Returns a list of records for the provided zone. + + :param zone: zone to list records for + :type zone: `Zone` + + :rtype: list of :class: `Record` + """ + if not isinstance(zone, Zone): + raise ZoneRequiredException('zone should be of type Zone') + + zones = self.list_zones() + + if not self.ex_zone_exists(zone.domain, zones): + raise ZoneDoesNotExistError(value='', driver=self, + zone_id=zone.domain) + + action = '/v1/dns/records' + params = {'domain': zone.domain} + response, errors = self.connection.request(action=action, + params=params).parse_body() + records = self._to_records(response[0], zone=zone) + + return records + + def get_zone(self, zone_id): + """ + Returns a `Zone` instance. + + :param zone_id: name of the zone user wants to get. + :type zone_id: ``str`` + + :rtype: :class:`Zone` + """ + ret_zone = None + + action = '/v1/dns/list' + params = {'api_key': self.key} + response, errors = self.connection.request(action=action, + params=params).parse_body() + zones = self._to_zones(response[0]) + + if not self.ex_zone_exists(zone_id, zones): + raise ZoneDoesNotExistError(value=None, zone_id=zone_id, + driver=self) + + for zone in zones: + if zone_id == zone.domain: + ret_zone = zone + + return ret_zone + + def get_record(self, zone_id, record_id): + """ + Returns a Record instance. + + :param zone_id: name of the required zone + :type zone_id: ``str`` + + :param record_id: ID of the required record + :type record_id: ``str`` + + :rtype: :class: `Record` + """ + ret_record = None + zone = self.get_zone(zone_id=zone_id) + records = self.list_records(zone=zone) + + if not self.ex_record_exists(record_id, records): + raise RecordDoesNotExistError(value='', driver=self, + record_id=record_id) + + for record in records: + if record_id == record.id: + ret_record = record + + return ret_record + + def create_zone(self, zone_id, type='master', ttl=None, extra={}): + """ + Returns a `Zone` object. + + :param zone_id: Zone domain name, (e.g. example.com). + :type zone_id: ``str`` + + :param type: Zone type (master / slave). + :type type: ``str`` + + :param ttl: TTL for new records. (optional) + :type ttl: ``int`` + + :param extra: (optional) Extra attributes (driver specific). + (e.g. {'serverip':'127.0.0.1'}) + """ + if extra and extra.get('serverip'): + serverip = extra['serverip'] + + params = {'api_key': self.key} + data = urllib.urlencode({'domain': zone_id, 'serverip': serverip}) + action = '/v1/dns/create_domain' + zones = self.list_zones() + if self.ex_zone_exists(zone_id, zones): + raise ZoneAlreadyExistsError(value='', driver=self, + zone_id=zone_id) + + self.connection.request(params=params, action=action, data=data, + method='POST') + zone = Zone(id=zone_id, domain=zone_id, type=type, ttl=ttl, + driver=self, extra=extra) + + return zone + + def create_record(self, name, zone, type, data, extra={}): + """ + Create a new record. + + :param name: Record name without the domain name (e.g. www). + Note: If you want to create a record for a base domain + name, you should specify empty string ('') for this + argument. + :type name: ``str`` + + :param zone: Zone where the requested record is created. + :type zone: :class:`Zone` + + :param type: DNS record type (A, AAAA, ...). + :type type: :class:`RecordType` + + :param data: Data for the record (depends on the record type). + :type data: ``str`` + + :param extra: Extra attributes (driver specific). (optional) + :type extra: ``dict`` + + :rtype: :class:`Record` + """ + ret_record = None + old_records_list = self.list_records(zone=zone) + # check if record already exists + # if exists raise RecordAlreadyExistsError + for record in old_records_list: + if record.name == name and record.data == data: + raise RecordAlreadyExistsError(value='', driver=self, + record_id=record.id) + + if extra and extra.get('priority'): + priority = int(extra['priority']) + + MX = self.RECORD_TYPE_MAP.get('MX') + SRV = self.RECORD_TYPE_MAP.get('SRV') + + post_data = {'domain': zone.domain, 'name': name, + 'type': self.RECORD_TYPE_MAP.get('type'), 'data': data} + + if type == MX or type == SRV: + post_data['priority'] = priority + + post_data = {'domain': zone.domain, 'name': name, + 'type': self.RECORD_TYPE_MAP.get(type), 'data': data} + + encoded_data = urllib.urlencode(post_data) + params = {'api_key': self.key} + action = '/v1/dns/create_record' + + response, errors = self.connection.request(action=action, + params=params, + data=encoded_data, + method='POST').parse_body() + updated_zone_records = zone.list_records() + + for record in updated_zone_records: + if record.name == name and record.data == data: + ret_record = record + + return ret_record + + def delete_zone(self, zone): + """ + Delete a zone. + + Note: This will delete all the records belonging to this zone. + + :param zone: Zone to delete. + :type zone: :class:`Zone` + + :rtype: ``bool`` + """ + action = '/v1/dns/delete_domain' + params = {'api_key': self.key} + data = urllib.urlencode({'domain': zone.domain}) + zones = self.list_zones() + if not self.ex_zone_exists(zone.domain, zones): + raise ZoneDoesNotExistError(value='', driver=self, + zone_id=zone.domain) + + response = self.connection.request(params=params, action=action, + data=data, method='POST') + + return response.status == 200 + + def delete_record(self, record): + """ + Delete a record. + + :param record: Record to delete. + :type record: :class:`Record` + + :rtype: ``bool`` + """ + action = '/v1/dns/delete_record' + params = {'api_key': self.key} + data = urllib.urlencode({'RECORDID': record.id, + 'domain': record.zone.domain}) + + zone_records = self.list_records(record.zone) + if not self.ex_record_exists(record.id, zone_records): + raise RecordDoesNotExistError(value='', driver=self, + record_id=record.id) + + response = self.connection.request(action=action, params=params, + data=data, method='POST') + + return response.status == 200 + + def ex_zone_exists(self, zone_id, zones_list): + """ + Function to check if a `Zone` object exists. + + :param zone_id: Name of the `Zone` object. + :type zone_id: ``str`` + + :param zones_list: A list containing `Zone` objects + :type zones_list: ``list`` + + :rtype: Returns `True` or `False` + """ + + zone_ids = [] + for zone in zones_list: + zone_ids.append(zone.domain) + + return zone_id in zone_ids + + def ex_record_exists(self, record_id, records_list): + """ + :param record_id: Name of the `Record` object. + :type record_id: ``str`` + + :param records_list: A list containing `Record` objects + :type records_list: ``list`` + + :rtype: ``bool`` + """ + record_ids = [] + for record in records_list: + record_ids.append(record.id) + + return record_id in record_ids + + def _to_zone(self, item): + """ + Build an object `Zone` from the item dictionary + + :param item: item to build the zone from + :type item: `dictionary` + + :rtype: :instance: `Zone` + """ + type = 'master' + extra = {'date_created': item['date_created']} + + zone = Zone(id=item['domain'], domain=item['domain'], driver=self, + type=type, ttl=None, extra=extra) + + return zone + + def _to_zones(self, items): + """ + Returns a list of `Zone` objects. + + :param: items: a list that contains dictionary objects to be passed + to the _to_zone function. + :type items: ``list`` + """ + zones = [] + for item in items: + zones.append(self._to_zone(item)) + + return zones + + def _to_record(self, item, zone): + + extra = {} + + if item.get('priority'): + extra['priority'] = item['priority'] + + type = self._string_to_record_type(item['type']) + record = Record(id=item['RECORDID'], name=item['name'], type=type, + data=item['data'], zone=zone, driver=self, extra=extra) + + return record + + def _to_records(self, items, zone): + records = [] + for item in items: + records.append(self._to_record(item, zone=zone)) + + return records diff --git a/libcloud/dns/providers.py b/libcloud/dns/providers.py index aef0aa7bd7..a4748c95eb 100644 --- a/libcloud/dns/providers.py +++ b/libcloud/dns/providers.py @@ -37,6 +37,8 @@ ('libcloud.dns.drivers.softlayer', 'SoftLayerDNSDriver'), Provider.DIGITAL_OCEAN: ('libcloud.dns.drivers.digitalocean', 'DigitalOceanDNSDriver'), + Provider.VULTR: + ('libcloud.dns.drivers.vultr', 'VultrDNSDriver'), # Deprecated Provider.RACKSPACE_US: ('libcloud.dns.drivers.rackspace', 'RackspaceUSDNSDriver'), diff --git a/libcloud/dns/types.py b/libcloud/dns/types.py index ad2b34a613..53e87ef926 100644 --- a/libcloud/dns/types.py +++ b/libcloud/dns/types.py @@ -39,6 +39,7 @@ class Provider(object): SOFTLAYER = 'softlayer' DIGITAL_OCEAN = 'digitalocean' AURORADNS = 'auroradns' + VULTR = 'vultr' # Deprecated RACKSPACE_US = 'rackspace_us' diff --git a/libcloud/test/dns/fixtures/vultr/delete_zone.json b/libcloud/test/dns/fixtures/vultr/delete_zone.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/libcloud/test/dns/fixtures/vultr/delete_zone.json @@ -0,0 +1 @@ +[] diff --git a/libcloud/test/dns/fixtures/vultr/empty_records_list.json b/libcloud/test/dns/fixtures/vultr/empty_records_list.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/libcloud/test/dns/fixtures/vultr/empty_records_list.json @@ -0,0 +1 @@ +[] diff --git a/libcloud/test/dns/fixtures/vultr/empty_zones_list.json b/libcloud/test/dns/fixtures/vultr/empty_zones_list.json new file mode 100644 index 0000000000..41b42e677b --- /dev/null +++ b/libcloud/test/dns/fixtures/vultr/empty_zones_list.json @@ -0,0 +1,3 @@ +[ + +] diff --git a/libcloud/test/dns/fixtures/vultr/get_record.json b/libcloud/test/dns/fixtures/vultr/get_record.json new file mode 100644 index 0000000000..2c7145145a --- /dev/null +++ b/libcloud/test/dns/fixtures/vultr/get_record.json @@ -0,0 +1,11 @@ +[ + { + "type":"A", + "name":"zupo", + "data":"127.0.0.1", + "priority":0, + "RECORDID":1300 + + } + +] diff --git a/libcloud/test/dns/fixtures/vultr/get_zone.json b/libcloud/test/dns/fixtures/vultr/get_zone.json new file mode 100644 index 0000000000..1d488272c2 --- /dev/null +++ b/libcloud/test/dns/fixtures/vultr/get_zone.json @@ -0,0 +1,7 @@ +[ + { + "domain":"zupo.com", + "date_created":"2015-11-12 16:58:59" + } + +] diff --git a/libcloud/test/dns/fixtures/vultr/list_domains.json b/libcloud/test/dns/fixtures/vultr/list_domains.json new file mode 100644 index 0000000000..644758ea7a --- /dev/null +++ b/libcloud/test/dns/fixtures/vultr/list_domains.json @@ -0,0 +1,22 @@ +[ + { + "domain": "example.com", + "date_created": "2014-12-11 16:20:59" + }, + { + "domain": "zupo.com", + "date_created": "2014-12-11 16:21:50" + }, + + { + "domain":"oltjano.com", + "date_created": "2014-12-11 16:21:40" + }, + + { + "domain":"13.com", + "date_created":"2015-12-11 16:21:50" + } + +] + diff --git a/libcloud/test/dns/fixtures/vultr/list_records.json b/libcloud/test/dns/fixtures/vultr/list_records.json new file mode 100644 index 0000000000..13548107a6 --- /dev/null +++ b/libcloud/test/dns/fixtures/vultr/list_records.json @@ -0,0 +1,19 @@ +[ + { + "type":"A", + "name":"arecord", + "data":"127.0.0.1", + "RECORDID":13 + }, + + { + "type":"CNAME", + "name":"*", + "data":"example.com", + "priority":0, + "RECORDID":1265277 + } + + + +] diff --git a/libcloud/test/dns/fixtures/vultr/test_zone.json b/libcloud/test/dns/fixtures/vultr/test_zone.json new file mode 100644 index 0000000000..e399fbbb72 --- /dev/null +++ b/libcloud/test/dns/fixtures/vultr/test_zone.json @@ -0,0 +1,9 @@ +[ + + { + "domain":"test.com", + "date_created":"1-1-2015 18:31:31" + + } + +] diff --git a/libcloud/test/dns/test_vultr.py b/libcloud/test/dns/test_vultr.py new file mode 100644 index 0000000000..2347a781c8 --- /dev/null +++ b/libcloud/test/dns/test_vultr.py @@ -0,0 +1,317 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import unittest + +from libcloud.dns.drivers.vultr import VultrDNSDriver +from libcloud.dns.types import RecordType +from libcloud.utils.py3 import httplib +from libcloud.test import MockHttp +from libcloud.test.secrets import VULTR_PARAMS +from libcloud.test.file_fixtures import DNSFileFixtures +from libcloud.dns.types import ZoneDoesNotExistError, RecordDoesNotExistError +from libcloud.dns.types import ZoneAlreadyExistsError +from libcloud.dns.base import Zone, Record + + +class VultrTests(unittest.TestCase): + + def setUp(self): + VultrMockHttp.type = None + VultrDNSDriver.connectionCls.conn_classes = ( + None, VultrMockHttp) + self.driver = VultrDNSDriver(*VULTR_PARAMS) + self.test_zone = Zone(id='test.com', type='master', ttl=None, + domain='test.com', extra={}, driver=self) + self.test_record = Record(id='31', type=RecordType.A, name='test', + zone=self.test_zone, data='127.0.0.1', + driver=self, extra={}) + + def test_list_zones_empty(self): + VultrMockHttp.type = 'EMPTY_ZONES_LIST' + zones = self.driver.list_zones() + + self.assertEqual(zones, []) + + def test_list_zones_success(self): + zones = self.driver.list_zones() + self.assertEqual(len(zones), 4) + + zone = zones[0] + self.assertEqual(zone.id, 'example.com') + self.assertEqual(zone.type, 'master') + self.assertEqual(zone.domain, 'example.com') + self.assertEqual(zone.ttl, None) + + zone = zones[1] + self.assertEqual(zone.id, 'zupo.com') + self.assertEqual(zone.type, 'master') + self.assertEqual(zone.domain, 'zupo.com') + self.assertEqual(zone.ttl, None) + + zone = zones[2] + self.assertEqual(zone.id, 'oltjano.com') + self.assertEqual(zone.type, 'master') + self.assertEqual(zone.domain, 'oltjano.com') + self.assertEqual(zone.ttl, None) + + zone = zones[3] + self.assertEqual(zone.id, '13.com') + self.assertEqual(zone.type, 'master') + self.assertEqual(zone.domain, '13.com') + self.assertEqual(zone.ttl, None) + + def test_get_zone_zone_does_not_exist(self): + VultrMockHttp.type = 'GET_ZONE_ZONE_DOES_NOT_EXIST' + try: + self.driver.get_zone(zone_id='test.com') + except ZoneDoesNotExistError: + e = sys.exc_info()[1] + self.assertEqual(e.zone_id, 'test.com') + else: + self.fail('Exception was not thrown') + + def test_get_zone_success(self): + VultrMockHttp.type = 'GET_ZONE_SUCCESS' + zone = self.driver.get_zone(zone_id='zupo.com') + + self.assertEqual(zone.id, 'zupo.com') + self.assertEqual(zone.domain, 'zupo.com') + self.assertEqual(zone.type, 'master') + self.assertEqual(zone.ttl, None) + + def test_delete_zone_zone_does_not_exist(self): + VultrMockHttp.type = 'DELETE_ZONE_ZONE_DOES_NOT_EXIST' + + try: + self.driver.delete_zone(zone=self.test_zone) + except ZoneDoesNotExistError: + e = sys.exc_info()[1] + self.assertEqual(e.zone_id, self.test_zone.id) + else: + self.fail('Exception was not thrown') + + def test_delete_zone_success(self): + zone = self.driver.list_zones()[0] + status = self.driver.delete_zone(zone=zone) + + self.assertTrue(status) + + def test_create_zone_success(self): + zone = self.driver.create_zone(zone_id='test.com', + extra={'serverip': '127.0.0.1'}) + + self.assertEqual(zone.id, 'test.com') + self.assertEqual(zone.domain, 'test.com') + self.assertEqual(zone.type, 'master'), + self.assertEqual(zone.ttl, None) + + def test_create_zone_zone_already_exists(self): + VultrMockHttp.type = 'CREATE_ZONE_ZONE_ALREADY_EXISTS' + + try: + self.driver.create_zone(zone_id='example.com', + extra={'serverip': '127.0.0.1'}) + except ZoneAlreadyExistsError: + e = sys.exc_info()[1] + self.assertEqual(e.zone_id, 'example.com') + else: + self.fail('Exception was not thrown') + + def test_get_record_record_does_not_exist(self): + VultrMockHttp.type = 'GET_RECORD_RECORD_DOES_NOT_EXIST' + + try: + self.driver.get_record(zone_id='zupo.com', record_id='1300') + except RecordDoesNotExistError: + e = sys.exc_info()[1] + self.assertEqual(e.record_id, '1300') + else: + self.fail('Exception was not thrown') + + def test_list_records_zone_does_not_exist(self): + VultrMockHttp.type = 'LIST_RECORDS_ZONE_DOES_NOT_EXIST' + + try: + self.driver.list_records(zone=self.test_zone) + except ZoneDoesNotExistError: + e = sys.exc_info()[1] + self.assertEqual(e.zone_id, self.test_zone.id) + else: + self.fail('Exception was not thrown') + + def test_list_records_empty(self): + VultrMockHttp.type = 'EMPTY_RECORDS_LIST' + zone = self.driver.list_zones()[0] + records = self.driver.list_records(zone=zone) + + self.assertEqual(records, []) + + def test_list_records_success(self): + zone = self.driver.get_zone(zone_id='zupo.com') + records = self.driver.list_records(zone=zone) + self.assertEqual(len(records), 2) + + arecord = records[0] + self.assertEqual(arecord.id, '13') + self.assertEqual(arecord.name, 'arecord') + self.assertEqual(arecord.type, RecordType.A) + self.assertEqual(arecord.data, '127.0.0.1') + + def test_get_record_success(self): + VultrMockHttp.type = 'GET_RECORD' + record = self.driver.get_record(zone_id='zupo.com', record_id='1300') + + self.assertEqual(record.id, '1300') + self.assertEqual(record.name, 'zupo') + self.assertEqual(record.data, '127.0.0.1') + self.assertEqual(record.type, RecordType.A) + + def test_delete_record_record_does_not_exist(self): + VultrMockHttp.type = 'DELETE_RECORD_RECORD_DOES_NOT_EXIST' + + try: + self.driver.delete_record(record=self.test_record) + except RecordDoesNotExistError: + e = sys.exc_info()[1] + self.assertEqual(e.record_id, self.test_record.id) + else: + self.fail('Exception was not thrown') + + def test_delete_record_success(self): + zone = self.driver.list_zones()[0] + record = self.driver.list_records(zone=zone)[0] + status = self.driver.delete_record(record=record) + + self.assertTrue(status) + + +class VultrMockHttp(MockHttp): + fixtures = DNSFileFixtures('vultr') + + def _v1_dns_list(self, method, url, body, headers): + body = self.fixtures.load('list_domains.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_records(self, method, url, body, headers): + body = self.fixtures.load('list_records.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_list_ZONE_DOES_NOT_EXIST(self, method, url, body, headers): + body = self.fixtures.load('list_domains.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_list_EMPTY_ZONES_LIST(self, method, url, body, headers): + body = self.fixtures.load('empty_zones_list.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_list_GET_ZONE_ZONE_DOES_NOT_EXIST(self, method, url, body, + headers): + body = self.fixtures.load('list_domains.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_list_GET_ZONE_SUCCESS(self, method, url, body, headers): + body = self.fixtures.load('get_zone.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_list_EMPTY_RECORDS_LIST(self, method, url, body, headers): + body = self.fixtures.load('list_domains.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_records_EMPTY_RECORDS_LIST(self, method, url, body, headers): + body = self.fixtures.load('empty_records_list.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_list_GET_RECORD(self, method, url, body, headers): + body = self.fixtures.load('get_zone.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_records_GET_RECORD(self, method, url, body, headers): + body = self.fixtures.load('get_record.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_list_GET_RECORD_RECORD_DOES_NOT_EXIST(self, method, url, body, + headers): + body = self.fixtures.load('get_zone.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_records_GET_RECORD_RECORD_DOES_NOT_EXIST(self, method, url, + body, headers): + body = self.fixtures.load('list_records.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_delete_domain(self, method, url, body, headers): + body = '' + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_delete_record(self, method, url, body, headers): + body = '' + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_create_domain(self, method, url, body, headers): + body = '' + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_list_CREATE_ZONE_ZONE_ALREADY_EXISTS(self, method, url, body, + headers): + body = self.fixtures.load('list_domains.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_create_domain_CREATE_ZONE_ZONE_ALREADY_EXISTS(self, method, + url, body, + headers): + body = '' + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_list_DELETE_ZONE_ZONE_DOES_NOT_EXIST(self, method, url, body, + headers): + body = self.fixtures.load('list_domains.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_delete_domain_DELETE_ZONE_ZONE_DOES_NOT_EXIST(self, method, + url, body, + headers): + body = '' + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_records_DELETE_RECORD_RECORD_DOES_NOT_EXIST(self, method, url, + body, headers): + body = self.fixtures.load('list_records.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_delete_record_DELETE_RECORD_RECORD_DOES_NOT_EXIST(self, method, + url, body, + headers): + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_list_DELETE_RECORD_RECORD_DOES_NOT_EXIST(self, method, url, + body, headers): + body = self.fixtures.load('test_zone.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_list_LIST_RECORDS_ZONE_DOES_NOT_EXIST(self, method, url, body, + headers): + body = self.fixtures.load('list_domains.json') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v1_dns_records_LIST_RECORDS_ZONE_DOES_NOT_EXIST(self, method, url, + body, headers): + body = '' + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + +if __name__ == '__main__': + sys.exit(unittest.main())