diff --git a/doc/source/user/guides/dns.rst b/doc/source/user/guides/dns.rst
new file mode 100644
index 000000000..f2ba4fbcf
--- /dev/null
+++ b/doc/source/user/guides/dns.rst
@@ -0,0 +1,18 @@
+Using OpenStack DNS
+===================
+
+Before working with the DNS service, you'll need to create a connection
+to your OpenStack cloud by following the :doc:`connect` user guide. This will
+provide you with the ``conn`` variable used in the examples below.
+
+.. TODO(gtema): Implement this guide
+
+List Zones
+----------
+
+.. literalinclude:: ../examples/dns/list.py
+   :pyobject: list_zones
+
+Full example: `dns resource list`_
+
+.. _dns resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/dns/list.py
diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst
index 0dcbb3a50..4fa8a02f6 100644
--- a/doc/source/user/index.rst
+++ b/doc/source/user/index.rst
@@ -39,6 +39,7 @@ approach, this is where you'll want to begin.
    Clustering <guides/clustering>
    Compute <guides/compute>
    Database <guides/database>
+   DNS <guides/dns>
    Identity <guides/identity>
    Image <guides/image>
    Key Manager <guides/key_manager>
@@ -98,6 +99,7 @@ control which services can be used.
    Clustering <proxies/clustering>
    Compute <proxies/compute>
    Database <proxies/database>
+   DNS <proxies/dns>
    Identity v2 <proxies/identity_v2>
    Identity v3 <proxies/identity_v3>
    Image v1 <proxies/image_v1>
@@ -130,6 +132,7 @@ The following services have exposed *Resource* classes.
    Clustering <resources/clustering/index>
    Compute <resources/compute/index>
    Database <resources/database/index>
+   DNS <resources/dns/index>
    Identity <resources/identity/index>
    Image <resources/image/index>
    Key Management <resources/key_manager/index>
diff --git a/doc/source/user/proxies/dns.rst b/doc/source/user/proxies/dns.rst
new file mode 100644
index 000000000..91a63de2b
--- /dev/null
+++ b/doc/source/user/proxies/dns.rst
@@ -0,0 +1,81 @@
+DNS API
+=======
+
+For details on how to use dns, see :doc:`/user/guides/dns`
+
+.. automodule:: openstack.dns.v2._proxy
+
+The DNS Class
+-------------
+
+The dns high-level interface is available through the ``dns``
+member of a :class:`~openstack.connection.Connection` object.  The
+``dns`` member will only be added if the service is detected.
+
+DNS Zone Operations
+^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: openstack.dns.v2._proxy.Proxy
+
+   .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone
+   .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone
+   .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone
+   .. automethod:: openstack.dns.v2._proxy.Proxy.find_zone
+   .. automethod:: openstack.dns.v2._proxy.Proxy.zones
+   .. automethod:: openstack.dns.v2._proxy.Proxy.abandon_zone
+   .. automethod:: openstack.dns.v2._proxy.Proxy.xfr_zone
+
+Recordset Operations
+^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: openstack.dns.v2._proxy.Proxy
+
+   .. automethod:: openstack.dns.v2._proxy.Proxy.create_recordset
+   .. automethod:: openstack.dns.v2._proxy.Proxy.update_recordset
+   .. automethod:: openstack.dns.v2._proxy.Proxy.get_recordset
+   .. automethod:: openstack.dns.v2._proxy.Proxy.delete_recordset
+   .. automethod:: openstack.dns.v2._proxy.Proxy.recordsets
+
+Zone Import Operations
+^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: openstack.dns.v2._proxy.Proxy
+
+   .. automethod:: openstack.dns.v2._proxy.Proxy.zone_imports
+   .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_import
+   .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_import
+   .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone_import
+
+Zone Export Operations
+^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: openstack.dns.v2._proxy.Proxy
+
+   .. automethod:: openstack.dns.v2._proxy.Proxy.zone_exports
+   .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_export
+   .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_export
+   .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_export_text
+   .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone_export
+
+FloatingIP Operations
+^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: openstack.dns.v2._proxy.Proxy
+
+   .. automethod:: openstack.dns.v2._proxy.Proxy.floating_ips
+   .. automethod:: openstack.dns.v2._proxy.Proxy.get_floating_ip
+   .. automethod:: openstack.dns.v2._proxy.Proxy.update_floating_ip
+
+Zone Transfer Operations
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. autoclass:: openstack.dns.v2._proxy.Proxy
+
+   .. automethod:: openstack.dns.v2._proxy.Proxy.zone_transfer_requests
+   .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_transfer_request
+   .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_transfer_request
+   .. automethod:: openstack.dns.v2._proxy.Proxy.update_zone_transfer_request
+   .. automethod:: openstack.dns.v2._proxy.Proxy.delete_zone_transfer_request
+   .. automethod:: openstack.dns.v2._proxy.Proxy.zone_transfer_accepts
+   .. automethod:: openstack.dns.v2._proxy.Proxy.get_zone_transfer_accept
+   .. automethod:: openstack.dns.v2._proxy.Proxy.create_zone_transfer_accept
diff --git a/doc/source/user/resources/dns/index.rst b/doc/source/user/resources/dns/index.rst
new file mode 100644
index 000000000..a8d0c9360
--- /dev/null
+++ b/doc/source/user/resources/dns/index.rst
@@ -0,0 +1,12 @@
+DNS Resources
+=============
+
+.. toctree::
+   :maxdepth: 1
+
+   v2/zone
+   v2/zone_transfer
+   v2/zone_export
+   v2/zone_import
+   v2/floating_ip
+   v2/recordset
diff --git a/doc/source/user/resources/dns/v2/floating_ip.rst b/doc/source/user/resources/dns/v2/floating_ip.rst
new file mode 100644
index 000000000..d616e71a9
--- /dev/null
+++ b/doc/source/user/resources/dns/v2/floating_ip.rst
@@ -0,0 +1,12 @@
+openstack.dns.v2.floating_ip
+============================
+
+.. automodule:: openstack.dns.v2.floating_ip
+
+The FloatingIP Class
+--------------------
+
+The ``DNS`` class inherits from :class:`~openstack.resource.Resource`.
+
+.. autoclass:: openstack.dns.v2.floating_ip.FloatingIP
+   :members:
diff --git a/doc/source/user/resources/dns/v2/recordset.rst b/doc/source/user/resources/dns/v2/recordset.rst
new file mode 100644
index 000000000..c02302f2d
--- /dev/null
+++ b/doc/source/user/resources/dns/v2/recordset.rst
@@ -0,0 +1,12 @@
+openstack.dns.v2.recordset
+==========================
+
+.. automodule:: openstack.dns.v2.recordset
+
+The Recordset Class
+-------------------
+
+The ``DNS`` class inherits from :class:`~openstack.resource.Resource`.
+
+.. autoclass:: openstack.dns.v2.recordset.Recordset
+   :members:
diff --git a/doc/source/user/resources/dns/v2/zone.rst b/doc/source/user/resources/dns/v2/zone.rst
new file mode 100644
index 000000000..634bd8f3f
--- /dev/null
+++ b/doc/source/user/resources/dns/v2/zone.rst
@@ -0,0 +1,12 @@
+openstack.dns.v2.zone
+==============================
+
+.. automodule:: openstack.dns.v2.zone
+
+The Zone Class
+--------------
+
+The ``DNS`` class inherits from :class:`~openstack.resource.Resource`.
+
+.. autoclass:: openstack.dns.v2.zone.Zone
+   :members:
diff --git a/doc/source/user/resources/dns/v2/zone_export.rst b/doc/source/user/resources/dns/v2/zone_export.rst
new file mode 100644
index 000000000..2c2baa3ee
--- /dev/null
+++ b/doc/source/user/resources/dns/v2/zone_export.rst
@@ -0,0 +1,12 @@
+openstack.dns.v2.zone_export
+============================
+
+.. automodule:: openstack.dns.v2.zone_export
+
+The ZoneExport Class
+--------------------
+
+The ``DNS`` class inherits from :class:`~openstack.resource.Resource`.
+
+.. autoclass:: openstack.dns.v2.zone_export.ZoneExport
+   :members:
diff --git a/doc/source/user/resources/dns/v2/zone_import.rst b/doc/source/user/resources/dns/v2/zone_import.rst
new file mode 100644
index 000000000..5836f539d
--- /dev/null
+++ b/doc/source/user/resources/dns/v2/zone_import.rst
@@ -0,0 +1,12 @@
+openstack.dns.v2.zone_import
+============================
+
+.. automodule:: openstack.dns.v2.zone_import
+
+The ZoneImport Class
+--------------------
+
+The ``DNS`` class inherits from :class:`~openstack.resource.Resource`.
+
+.. autoclass:: openstack.dns.v2.zone_import.ZoneImport
+   :members:
diff --git a/doc/source/user/resources/dns/v2/zone_transfer.rst b/doc/source/user/resources/dns/v2/zone_transfer.rst
new file mode 100644
index 000000000..9f5c2c4c4
--- /dev/null
+++ b/doc/source/user/resources/dns/v2/zone_transfer.rst
@@ -0,0 +1,20 @@
+openstack.dns.v2.zone_transfer
+==============================
+
+.. automodule:: openstack.dns.v2.zone_transfer
+
+The ZoneTransferRequest Class
+-----------------------------
+
+The ``DNS`` class inherits from :class:`~openstack.resource.Resource`.
+
+.. autoclass:: openstack.dns.v2.zone_transfer.ZoneTransferRequest
+   :members:
+
+The ZoneTransferAccept Class
+----------------------------
+
+The ``DNS`` class inherits from :class:`~openstack.resource.Resource`.
+
+.. autoclass:: openstack.dns.v2.zone_transfer.ZoneTransferAccept
+   :members:
diff --git a/examples/dns/__init__.py b/examples/dns/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/examples/dns/list.py b/examples/dns/list.py
new file mode 100644
index 000000000..47024801e
--- /dev/null
+++ b/examples/dns/list.py
@@ -0,0 +1,24 @@
+# Licensed 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.
+
+"""
+List resources from the DNS service.
+
+For a full guide see TODO(gtema):link to docs on developer.openstack.org
+"""
+
+
+def list_zones(conn):
+    print("List Zones:")
+
+    for zone in conn.dns.zones():
+        print(zone)
diff --git a/openstack/dns/__init__.py b/openstack/dns/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/openstack/dns/dns_service.py b/openstack/dns/dns_service.py
new file mode 100644
index 000000000..6fa162b57
--- /dev/null
+++ b/openstack/dns/dns_service.py
@@ -0,0 +1,22 @@
+# Licensed 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.
+
+from openstack.dns.v2 import _proxy
+from openstack import service_description
+
+
+class DnsService(service_description.ServiceDescription):
+    """The DNS service."""
+
+    supported_versions = {
+        '2': _proxy.Proxy,
+    }
diff --git a/openstack/dns/v2/__init__.py b/openstack/dns/v2/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py
new file mode 100644
index 000000000..7e36b0c5e
--- /dev/null
+++ b/openstack/dns/v2/_proxy.py
@@ -0,0 +1,494 @@
+# Licensed 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.
+from openstack import proxy
+from openstack.dns.v2 import recordset as _rs
+from openstack.dns.v2 import zone as _zone
+from openstack.dns.v2 import zone_import as _zone_import
+from openstack.dns.v2 import zone_export as _zone_export
+from openstack.dns.v2 import zone_transfer as _zone_transfer
+from openstack.dns.v2 import floating_ip as _fip
+
+
+class Proxy(proxy.Proxy):
+
+    # ======== Zones ========
+    def zones(self, **query):
+        """Retrieve a generator of zones
+
+        :param dict query: Optional query parameters to be sent to limit the
+            resources being returned.
+
+            * `name`: Zone Name field.
+            * `type`: Zone Type field.
+            * `email`: Zone email field.
+            * `status`: Status of the zone.
+            * `ttl`: TTL field filter.abs
+            * `description`: Zone description field filter.
+
+        :returns: A generator of zone
+            :class:`~openstack.dns.v2.zone.Zone` instances.
+        """
+        return self._list(_zone.Zone, **query)
+
+    def create_zone(self, **attrs):
+        """Create a new zone from attributes
+
+        :param dict attrs: Keyword arguments which will be used to create
+            a :class:`~openstack.dns.v2.zone.Zone`,
+            comprised of the properties on the Zone class.
+        :returns: The results of zone creation.
+        :rtype: :class:`~openstack.dns.v2.zone.Zone`
+        """
+        return self._create(_zone.Zone, prepend_key=False, **attrs)
+
+    def get_zone(self, zone):
+        """Get a zone
+
+        :param zone: The value can be the ID of a zone
+             or a :class:`~openstack.dns.v2.zone.Zone` instance.
+        :returns: Zone instance.
+        :rtype: :class:`~openstack.dns.v2.zone.Zone`
+        """
+        return self._get(_zone.Zone, zone)
+
+    def delete_zone(self, zone, ignore_missing=True):
+        """Delete a zone
+
+        :param zone: The value can be the ID of a zone
+             or a :class:`~openstack.dns.v2.zone.Zone` instance.
+        :param bool ignore_missing: When set to ``False``
+            :class:`~openstack.exceptions.ResourceNotFound` will be raised when
+            the zone does not exist.
+            When set to ``True``, no exception will be set when attempting to
+            delete a nonexistent zone.
+
+        :returns: Zone been deleted
+        :rtype: :class:`~openstack.dns.v2.zone.Zone`
+        """
+        return self._delete(_zone.Zone, zone, ignore_missing=ignore_missing)
+
+    def update_zone(self, zone, **attrs):
+        """Update zone attributes
+
+        :param zone: The id or an instance of
+            :class:`~openstack.dns.v2.zone.Zone`.
+        :param dict attrs: attributes for update on
+            :class:`~openstack.dns.v2.zone.Zone`.
+
+        :rtype: :class:`~openstack.dns.v2.zone.Zone`
+        """
+        return self._update(_zone.Zone, zone, **attrs)
+
+    def find_zone(self, name_or_id, ignore_missing=True):
+        """Find a single zone
+
+        :param name_or_id: The name or ID of a zone
+        :param bool ignore_missing: When set to ``False``
+            :class:`~openstack.exceptions.ResourceNotFound` will be raised
+            when the zone does not exist.
+            When set to ``True``, no exception will be set when attempting
+            to delete a nonexistent zone.
+
+        :returns: :class:`~openstack.dns.v2.zone.Zone`
+        """
+        return self._find(_zone.Zone, name_or_id,
+                          ignore_missing=ignore_missing)
+
+    def abandon_zone(self, zone, **attrs):
+        """Abandon Zone
+
+        :param zone: The value can be the ID of a zone to be abandoned
+             or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance.
+
+        :returns: None
+        """
+        zone = self._get_resource(_zone.Zone, zone)
+
+        return zone.abandon(self)
+
+    def xfr_zone(self, zone, **attrs):
+        """Trigger update of secondary Zone
+
+        :param zone: The value can be the ID of a zone to be abandoned
+             or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance.
+
+        :returns: None
+        """
+        zone = self._get_resource(_zone.Zone, zone)
+        return zone.xfr(self)
+
+    # ======== Recordsets ========
+    def recordsets(self, zone=None, **query):
+        """Retrieve a generator of recordsets
+
+        :param zone: The optional value can be the ID of a zone
+             or a :class:`~openstack.dns.v2.zone.Zone` instance. If it is not
+             given all recordsets for all zones of the tenant would be
+             retrieved
+        :param dict query: Optional query parameters to be sent to limit the
+            resources being returned.
+
+            * `name`: Recordset Name field.
+            * `type`: Type field.
+            * `status`: Status of the recordset.
+            * `ttl`: TTL field filter.
+            * `description`: Recordset description field filter.
+
+        :returns: A generator of zone
+            (:class:`~openstack.dns.v2.recordset.Recordset`) instances
+        """
+        base_path = None
+        if not zone:
+            base_path = '/recordsets'
+        else:
+            zone = self._get_resource(_zone.Zone, zone)
+            query.update({'zone_id': zone.id})
+        return self._list(_rs.Recordset, base_path=base_path, **query)
+
+    def create_recordset(self, zone, **attrs):
+        """Create a new recordset in the zone
+
+        :param zone: The value can be the ID of a zone
+            or a :class:`~openstack.dns.v2.zone.Zone` instance.
+        :param dict attrs: Keyword arguments which will be used to create
+            a :class:`~openstack.dns.v2.recordset.Recordset`,
+            comprised of the properties on the Recordset class.
+        :returns: The results of zone creation
+        :rtype: :class:`~openstack.dns.v2.recordset.Recordset`
+        """
+        zone = self._get_resource(_zone.Zone, zone)
+        attrs.update({'zone_id': zone.id})
+        return self._create(_rs.Recordset, prepend_key=False, **attrs)
+
+    def update_recordset(self, recordset, **attrs):
+        """Update Recordset attributes
+
+        :param dict attrs: Keyword arguments which will be used to create
+            a :class:`~openstack.dns.v2.recordset.Recordset`,
+            comprised of the properties on the Recordset class.
+        :returns: The results of zone creation
+        :rtype: :class:`~openstack.dns.v2.recordset.Recordset`
+        """
+        return self._update(_rs.Recordset, recordset, **attrs)
+
+    def get_recordset(self, recordset, zone):
+        """Get a recordset
+
+        :param zone: The value can be the ID of a zone
+             or a :class:`~openstack.dns.v2.zone.Zone` instance.
+        :param recordset: The value can be the ID of a recordset
+             or a :class:`~openstack.dns.v2.recordset.Recordset` instance.
+        :returns: Recordset instance
+        :rtype: :class:`~openstack.dns.v2.recordset.Recordset`
+        """
+        zone = self._get_resource(_zone.Zone, zone)
+        return self._get(_rs.Recordset, recordset, zone_id=zone.id)
+
+    def delete_recordset(self, recordset, zone=None, ignore_missing=True):
+        """Delete a zone
+
+        :param recordset: The value can be the ID of a recordset
+             or a :class:`~openstack.dns.v2.recordset.Recordset`
+             instance.
+        :param zone: The value can be the ID of a zone
+             or a :class:`~openstack.dns.v2.zone.Zone` instance.
+        :param bool ignore_missing: When set to ``False``
+            :class:`~openstack.exceptions.ResourceNotFound` will be raised when
+            the zone does not exist. When set to ``True``, no exception will
+            be set when attempting to delete a nonexistent zone.
+
+        :returns: Recordset instance been deleted
+        :rtype: :class:`~openstack.dns.v2.recordset.Recordset`
+        """
+        if zone:
+            zone = self._get_resource(_zone.Zone, zone)
+            recordset = self._get(
+                _rs.Recordset, recordset, zone_id=zone.id)
+        return self._delete(_rs.Recordset, recordset,
+                            ignore_missing=ignore_missing)
+
+    # ======== Zone Imports ========
+    def zone_imports(self, **query):
+        """Retrieve a generator of zone imports
+
+        :param dict query: Optional query parameters to be sent to limit the
+            resources being returned.
+
+            * `zone_id`: Zone I field.
+            * `message`: Message field.
+            * `status`: Status of the zone import record.
+
+        :returns: A generator of zone
+            :class:`~openstack.dns.v2.zone_import.ZoneImport` instances.
+        """
+        return self._list(_zone_import.ZoneImport, **query)
+
+    def create_zone_import(self, **attrs):
+        """Create a new zone import from attributes
+
+        :param dict attrs: Keyword arguments which will be used to create
+            a :class:`~openstack.dns.v2.zone_import.ZoneImport`,
+            comprised of the properties on the ZoneImport class.
+        :returns: The results of zone creation.
+        :rtype: :class:`~openstack.dns.v2.zone_import.ZoneImport`
+        """
+        return self._create(_zone_import.ZoneImport, prepend_key=False,
+                            **attrs)
+
+    def get_zone_import(self, zone_import):
+        """Get a zone import record
+
+        :param zone: The value can be the ID of a zone import
+             or a :class:`~openstack.dns.v2.zone_import.ZoneImport` instance.
+        :returns: ZoneImport instance.
+        :rtype: :class:`~openstack.dns.v2.zone_import.ZoneImport`
+        """
+        return self._get(_zone_import.ZoneImport, zone_import)
+
+    def delete_zone_import(self, zone_import, ignore_missing=True):
+        """Delete a zone import
+
+        :param zone_import: The value can be the ID of a zone import
+             or a :class:`~openstack.dns.v2.zone_import.ZoneImport` instance.
+        :param bool ignore_missing: When set to ``False``
+            :class:`~openstack.exceptions.ResourceNotFound` will be raised when
+            the zone does not exist.
+            When set to ``True``, no exception will be set when attempting to
+            delete a nonexistent zone.
+
+        :returns: None
+        """
+        return self._delete(_zone_import.ZoneImport, zone_import,
+                            ignore_missing=ignore_missing)
+
+    # ======== Zone Exports ========
+    def zone_exports(self, **query):
+        """Retrieve a generator of zone exports
+
+        :param dict query: Optional query parameters to be sent to limit the
+            resources being returned.
+
+            * `zone_id`: Zone I field.
+            * `message`: Message field.
+            * `status`: Status of the zone import record.
+
+        :returns: A generator of zone
+            :class:`~openstack.dns.v2.zone_export.ZoneExport` instances.
+        """
+        return self._list(_zone_export.ZoneExport, **query)
+
+    def create_zone_export(self, zone, **attrs):
+        """Create a new zone export from attributes
+
+        :param zone: The value can be the ID of a zone to be exported
+             or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance.
+        :param dict attrs: Keyword arguments which will be used to create
+            a :class:`~openstack.dns.v2.zone_export.ZoneExport`,
+            comprised of the properties on the ZoneExport class.
+        :returns: The results of zone creation.
+        :rtype: :class:`~openstack.dns.v2.zone_export.ZoneExport`
+        """
+        zone = self._get_resource(_zone.Zone, zone)
+        return self._create(_zone_export.ZoneExport,
+                            base_path='/zones/%(zone_id)s/tasks/export',
+                            prepend_key=False,
+                            zone_id=zone.id,
+                            **attrs)
+
+    def get_zone_export(self, zone_export):
+        """Get a zone export record
+
+        :param zone: The value can be the ID of a zone import
+             or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance.
+        :returns: ZoneExport instance.
+        :rtype: :class:`~openstack.dns.v2.zone_export.ZoneExport`
+        """
+        return self._get(_zone_export.ZoneExport, zone_export)
+
+    def get_zone_export_text(self, zone_export):
+        """Get a zone export record as text
+
+        :param zone: The value can be the ID of a zone import
+             or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance.
+        :returns: ZoneExport instance.
+        :rtype: :class:`~openstack.dns.v2.zone_export.ZoneExport`
+        """
+        return self._get(_zone_export.ZoneExport, zone_export,
+                         base_path='/zones/tasks/export/%(id)s/export')
+
+    def delete_zone_export(self, zone_export, ignore_missing=True):
+        """Delete a zone export
+
+        :param zone_export: The value can be the ID of a zone import
+             or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance.
+        :param bool ignore_missing: When set to ``False``
+            :class:`~openstack.exceptions.ResourceNotFound` will be raised when
+            the zone does not exist.
+            When set to ``True``, no exception will be set when attempting to
+            delete a nonexistent zone.
+
+        :returns: None
+        """
+        return self._delete(_zone_export.ZoneExport, zone_export,
+                            ignore_missing=ignore_missing)
+
+    # ======== FloatingIPs ========
+    def floating_ips(self, **query):
+        """Retrieve a generator of recordsets
+
+        :param dict query: Optional query parameters to be sent to limit the
+            resources being returned.
+
+            * `name`: Recordset Name field.
+            * `type`: Type field.
+            * `status`: Status of the recordset.
+            * `ttl`: TTL field filter.
+            * `description`: Recordset description field filter.
+
+        :returns: A generator of floatingips
+            (:class:`~openstack.dns.v2.floating_ip.FloatingIP`) instances
+        """
+        return self._list(_fip.FloatingIP, **query)
+
+    def get_floating_ip(self, floating_ip):
+        """Get a Floating IP
+
+        :param floating_ip: The value can be the ID of a floating ip
+             or a :class:`~openstack.dns.v2.floating_ip.FloatingIP` instance.
+             The ID is in format "region_name:floatingip_id"
+        :returns: FloatingIP instance.
+        :rtype: :class:`~openstack.dns.v2.floating_ip.FloatingIP`
+        """
+        return self._get(_fip.FloatingIP, floating_ip)
+
+    def update_floating_ip(self, floating_ip, **attrs):
+        """Update floating ip attributes
+
+        :param floating_ip: The id or an instance of
+            :class:`~openstack.dns.v2.fip.FloatingIP`.
+        :param dict attrs: attributes for update on
+            :class:`~openstack.dns.v2.fip.FloatingIP`.
+
+        :rtype: :class:`~openstack.dns.v2.fip.FloatingIP`
+        """
+        return self._update(_fip.FloatingIP, floating_ip, **attrs)
+
+    # ======== Zone Transfer ========
+    def zone_transfer_requests(self, **query):
+        """Retrieve a generator of zone transfer requests
+
+        :param dict query: Optional query parameters to be sent to limit the
+            resources being returned.
+
+            * `status`: Status of the recordset.
+
+        :returns: A generator of transfer requests
+            (:class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`)
+            instances
+        """
+        return self._list(_zone_transfer.ZoneTransferRequest, **query)
+
+    def get_zone_transfer_request(self, request):
+        """Get a ZoneTransfer Request info
+
+        :param request: The value can be the ID of a transfer request
+             or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`
+             instance.
+        :returns: Zone transfer request instance.
+        :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`
+        """
+        return self._get(_zone_transfer.ZoneTransferRequest, request)
+
+    def create_zone_transfer_request(self, zone, **attrs):
+        """Create a new ZoneTransfer Request from attributes
+
+        :param zone: The value can be the ID of a zone to be transferred
+             or a :class:`~openstack.dns.v2.zone_export.ZoneExport` instance.
+        :param dict attrs: Keyword arguments which will be used to create
+            a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`,
+            comprised of the properties on the ZoneTransferRequest class.
+        :returns: The results of zone transfer request creation.
+        :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`
+        """
+        zone = self._get_resource(_zone.Zone, zone)
+        return self._create(
+            _zone_transfer.ZoneTransferRequest,
+            base_path='/zones/%(zone_id)s/tasks/transfer_requests',
+            prepend_key=False,
+            zone_id=zone.id,
+            **attrs)
+
+    def update_zone_transfer_request(self, request, **attrs):
+        """Update ZoneTransfer Request attributes
+
+        :param floating_ip: The id or an instance of
+            :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`.
+        :param dict attrs: attributes for update on
+            :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`.
+
+        :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`
+        """
+        return self._update(_zone_transfer.ZoneTransferRequest,
+                            request, **attrs)
+
+    def delete_zone_transfer_request(self, request, ignore_missing=True):
+        """Delete a ZoneTransfer Request
+
+        :param request: The value can be the ID of a zone transfer request
+             or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferRequest`
+             instance.
+        :param bool ignore_missing: When set to ``False``
+            :class:`~openstack.exceptions.ResourceNotFound` will be raised when
+            the zone does not exist.
+            When set to ``True``, no exception will be set when attempting to
+            delete a nonexistent zone.
+
+        :returns: None
+        """
+        return self._delete(_zone_transfer.ZoneTransferRequest, request,
+                            ignore_missing=ignore_missing)
+
+    def zone_transfer_accepts(self, **query):
+        """Retrieve a generator of zone transfer accepts
+
+        :param dict query: Optional query parameters to be sent to limit the
+            resources being returned.
+
+            * `status`: Status of the recordset.
+
+        :returns: A generator of transfer accepts
+            (:class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept`)
+            instances
+        """
+        return self._list(_zone_transfer.ZoneTransferAccept, **query)
+
+    def get_zone_transfer_accept(self, accept):
+        """Get a ZoneTransfer Accept info
+
+        :param request: The value can be the ID of a transfer accept
+             or a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept`
+             instance.
+        :returns: Zone transfer request instance.
+        :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept`
+        """
+        return self._get(_zone_transfer.ZoneTransferAccept, accept)
+
+    def create_zone_transfer_accept(self, **attrs):
+        """Create a new ZoneTransfer Accept from attributes
+
+        :param dict attrs: Keyword arguments which will be used to create
+            a :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept`,
+            comprised of the properties on the ZoneTransferAccept class.
+        :returns: The results of zone transfer request creation.
+        :rtype: :class:`~openstack.dns.v2.zone_transfer.ZoneTransferAccept`
+        """
+        return self._create(_zone_transfer.ZoneTransferAccept, **attrs)
diff --git a/openstack/dns/v2/floating_ip.py b/openstack/dns/v2/floating_ip.py
new file mode 100644
index 000000000..f6b4eec97
--- /dev/null
+++ b/openstack/dns/v2/floating_ip.py
@@ -0,0 +1,40 @@
+# Licensed 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.
+# from openstack import exceptions
+from openstack import resource
+
+
+class FloatingIP(resource.Resource):
+    """DNS Floating IP Resource"""
+    resource_key = ''
+    resources_key = 'floatingips'
+    base_path = '/reverse/floatingips'
+
+    # capabilities
+    allow_fetch = True
+    allow_commit = True
+    allow_list = True
+    commit_method = "PATCH"
+
+    #: Properties
+    #: current action in progress on the resource
+    action = resource.Body('action')
+    #: The floatingip address for this PTR record
+    address = resource.Body('address')
+    #: Description for this PTR record
+    description = resource.Body('description')
+    #: Domain name for this PTR record
+    ptrdname = resource.Body('ptrdname')
+    #: status of the resource
+    status = resource.Body('status')
+    #: Time to live for this PTR record
+    ttl = resource.Body('ttl', type=int)
diff --git a/openstack/dns/v2/recordset.py b/openstack/dns/v2/recordset.py
new file mode 100644
index 000000000..949b25d7e
--- /dev/null
+++ b/openstack/dns/v2/recordset.py
@@ -0,0 +1,64 @@
+# Licensed 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.
+# from openstack import exceptions
+from openstack import resource
+
+
+class Recordset(resource.Resource):
+    """DNS Recordset Resource"""
+    resource_key = 'recordset'
+    resources_key = 'recordsets'
+    base_path = '/zones/%(zone_id)s/recordsets'
+
+    # capabilities
+    allow_create = True
+    allow_fetch = True
+    allow_commit = True
+    allow_delete = True
+    allow_list = True
+
+    _query_mapping = resource.QueryParameters(
+        'name', 'type', 'ttl', 'data', 'status', 'description',
+        'limit', 'marker')
+
+    #: Properties
+    #: current action in progress on the resource
+    action = resource.Body('action')
+    #: Timestamp when the zone was created
+    created_at = resource.Body('create_at')
+    #: Recordset description
+    description = resource.Body('description')
+    #: Links contains a `self` pertaining to this zone or a `next` pertaining
+    #: to next page
+    links = resource.Body('links', type=dict)
+    #: DNS Name of the recordset
+    name = resource.Body('name')
+    #: ID of the project which the recordset belongs to
+    project_id = resource.Body('project_id')
+    #: DNS record value list
+    records = resource.Body('records', type=list)
+    #: Recordset status
+    #: Valid values include: `PENDING_CREATE`, `ACTIVE`,`PENDING_DELETE`,
+    #: `ERROR`
+    status = resource.Body('status')
+    #: Time to live, default 300, available value 300-2147483647 (seconds)
+    ttl = resource.Body('ttl', type=int)
+    #: DNS type of the recordset
+    #: Valid values include `A`, `AAAA`, `MX`, `CNAME`, `TXT`, `NS`,
+    #: `SSHFP`, `SPF`, `SRV`, `PTR`
+    type = resource.Body('type')
+    #: Timestamp when the zone was last updated
+    updated_at = resource.Body('updated_at')
+    #: The id of the Zone which this recordset belongs to
+    zone_id = resource.URI('zone_id')
+    #: The name of the Zone which this recordset belongs to
+    zone_name = resource.Body('zone_name')
diff --git a/openstack/dns/v2/zone.py b/openstack/dns/v2/zone.py
new file mode 100644
index 000000000..737792bcf
--- /dev/null
+++ b/openstack/dns/v2/zone.py
@@ -0,0 +1,96 @@
+# Licensed 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.
+from openstack import exceptions
+from openstack import resource
+from openstack import utils
+
+
+class Zone(resource.Resource):
+    """DNS ZONE Resource"""
+    resources_key = 'zones'
+    base_path = '/zones'
+
+    # capabilities
+    allow_create = True
+    allow_fetch = True
+    allow_commit = True
+    allow_delete = True
+    allow_list = True
+
+    commit_method = "PATCH"
+
+    _query_mapping = resource.QueryParameters(
+        'name', 'type', 'email', 'status', 'description', 'ttl',
+        'limit', 'marker'
+    )
+
+    #: Properties
+    #: current action in progress on the resource
+    action = resource.Body('action')
+    #: Attributes
+    #: Key:Value pairs of information about this zone, and the pool the user
+    #: would like to place the zone in. This information can be used by the
+    #: scheduler to place zones on the correct pool.
+    attributes = resource.Body('attributes', type=dict)
+    #: Timestamp when the zone was created
+    created_at = resource.Body('created_at')
+    #: Zone description
+    #: *Type: str*
+    description = resource.Body('description')
+    #: The administrator email of this zone
+    #: *Type: str*
+    email = resource.Body('email')
+    #: Links contains a `self` pertaining to this zone or a `next` pertaining
+    #: to next page
+    links = resource.Body('links', type=dict)
+    #: The master list for slaver server to fetch DNS
+    masters = resource.Body('masters', type=list)
+    #: Zone name
+    name = resource.Body('name')
+    #: The pool which manages the zone, assigned by system
+    pool_id = resource.Body('pool_id')
+    #: The project id which the zone belongs to
+    project_id = resource.Body('project_id')
+    #: Serial number in the SOA record set in the zone,
+    #: which identifies the change on the primary DNS server
+    #: *Type: int*
+    serial = resource.Body('serial', type=int)
+    #: Zone status
+    #: Valid values include `PENDING_CREATE`, `ACTIVE`,
+    #: `PENDING_DELETE`, `ERROR`
+    status = resource.Body('status')
+    #: SOA TTL time, unit is seconds, default 300, TTL range 300-2147483647
+    #: *Type: int*
+    ttl = resource.Body('ttl', type=int)
+    #: Zone type,
+    #: Valid values include `PRIMARY`, `SECONDARY`
+    #: *Type: str*
+    type = resource.Body('type')
+    #: Timestamp when the zone was last updated
+    updated_at = resource.Body('updated_at')
+
+    def _action(self, session, action, body):
+        """Preform actions given the message body.
+
+        """
+        url = utils.urljoin(self.base_path, self.id, 'tasks', action)
+        response = session.post(
+            url,
+            json=body)
+        exceptions.raise_from_response(response)
+        return response
+
+    def abandon(self, session):
+        self._action(session, 'abandon', None)
+
+    def xfr(self, session):
+        self._action(session, 'xfr', None)
diff --git a/openstack/dns/v2/zone_export.py b/openstack/dns/v2/zone_export.py
new file mode 100644
index 000000000..6cd5c9232
--- /dev/null
+++ b/openstack/dns/v2/zone_export.py
@@ -0,0 +1,86 @@
+# Licensed 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.
+from openstack import exceptions
+from openstack import resource
+
+
+class ZoneExport(resource.Resource):
+    """DNS Zone Exports Resource"""
+    resource_key = ''
+    resources_key = 'exports'
+    base_path = '/zones/tasks/export'
+
+    # capabilities
+    allow_create = True
+    allow_fetch = True
+    allow_delete = True
+    allow_list = True
+
+    _query_mapping = resource.QueryParameters(
+        'zone_id', 'message', 'status'
+    )
+
+    #: Properties
+    #: Timestamp when the zone was created
+    created_at = resource.Body('created_at')
+    #: Links contains a `self` pertaining to this zone or a `next` pertaining
+    #: to next page
+    links = resource.Body('links', type=dict)
+    #: Message
+    message = resource.Body('message')
+    #: Returns the total_count of resources matching this filter
+    metadata = resource.Body('metadata', type=list)
+    #: The project id which the zone belongs to
+    project_id = resource.Body('project_id')
+    #: Current status of the zone export
+    status = resource.Body('status')
+    #: Timestamp when the zone was last updated
+    updated_at = resource.Body('updated_at')
+    #: Version of the resource
+    version = resource.Body('version', type=int)
+    #: ID for the zone that was created by this export
+    zone_id = resource.Body('zone_id')
+
+    def create(self, session, prepend_key=True, base_path=None):
+        """Create a remote resource based on this instance.
+
+        :param session: The session to use for making this request.
+        :type session: :class:`~keystoneauth1.adapter.Adapter`
+        :param prepend_key: A boolean indicating whether the resource_key
+                            should be prepended in a resource creation
+                            request. Default to True.
+        :param str base_path: Base part of the URI for creating resources, if
+                              different from
+                              :data:`~openstack.resource.Resource.base_path`.
+        :return: This :class:`Resource` instance.
+        :raises: :exc:`~openstack.exceptions.MethodNotSupported` if
+                 :data:`Resource.allow_create` is not set to ``True``.
+        """
+        if not self.allow_create:
+            raise exceptions.MethodNotSupported(self, "create")
+
+        session = self._get_session(session)
+        microversion = self._get_microversion_for(session, 'create')
+        # Create ZoneExport requires empty body
+        # skip _prepare_request completely, since we need just empty body
+        request = resource._Request(
+            self.base_path,
+            None,
+            None
+        )
+        response = session.post(request.url,
+                                json=request.body, headers=request.headers,
+                                microversion=microversion)
+
+        self.microversion = microversion
+        self._translate_response(response)
+        return self
diff --git a/openstack/dns/v2/zone_import.py b/openstack/dns/v2/zone_import.py
new file mode 100644
index 000000000..5a18bdcc7
--- /dev/null
+++ b/openstack/dns/v2/zone_import.py
@@ -0,0 +1,86 @@
+# Licensed 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.
+from openstack import exceptions
+from openstack import resource
+
+
+class ZoneImport(resource.Resource):
+    """DNS Zone Import Resource"""
+    resource_key = ''
+    resources_key = 'imports'
+    base_path = '/zones/tasks/import'
+
+    # capabilities
+    allow_create = True
+    allow_fetch = True
+    allow_delete = True
+    allow_list = True
+
+    _query_mapping = resource.QueryParameters(
+        'zone_id', 'message', 'status'
+    )
+
+    #: Properties
+    #: Timestamp when the zone was created
+    created_at = resource.Body('created_at')
+    #: Links contains a `self` pertaining to this zone or a `next` pertaining
+    #: to next page
+    links = resource.Body('links', type=dict)
+    #: Message
+    message = resource.Body('message')
+    #: Returns the total_count of resources matching this filter
+    metadata = resource.Body('metadata', type=list)
+    #: The project id which the zone belongs to
+    project_id = resource.Body('project_id')
+    #: Current status of the zone import
+    status = resource.Body('status')
+    #: Timestamp when the zone was last updated
+    updated_at = resource.Body('updated_at')
+    #: Version of the resource
+    version = resource.Body('version', type=int)
+    #: ID for the zone that was created by this import
+    zone_id = resource.Body('zone_id')
+
+    def create(self, session, prepend_key=True, base_path=None):
+        """Create a remote resource based on this instance.
+
+        :param session: The session to use for making this request.
+        :type session: :class:`~keystoneauth1.adapter.Adapter`
+        :param prepend_key: A boolean indicating whether the resource_key
+                            should be prepended in a resource creation
+                            request. Default to True.
+        :param str base_path: Base part of the URI for creating resources, if
+                              different from
+                              :data:`~openstack.resource.Resource.base_path`.
+        :return: This :class:`Resource` instance.
+        :raises: :exc:`~openstack.exceptions.MethodNotSupported` if
+                 :data:`Resource.allow_create` is not set to ``True``.
+        """
+        if not self.allow_create:
+            raise exceptions.MethodNotSupported(self, "create")
+
+        session = self._get_session(session)
+        microversion = self._get_microversion_for(session, 'create')
+        # Create ZoneImport requires empty body and 'text/dns' as content-type
+        # skip _prepare_request completely, since we need just empty body
+        request = resource._Request(
+            self.base_path,
+            None,
+            {'content-type': 'text/dns'}
+        )
+        response = session.post(request.url,
+                                json=request.body, headers=request.headers,
+                                microversion=microversion)
+
+        self.microversion = microversion
+        self._translate_response(response)
+        return self
diff --git a/openstack/dns/v2/zone_transfer.py b/openstack/dns/v2/zone_transfer.py
new file mode 100644
index 000000000..2bd95fe3c
--- /dev/null
+++ b/openstack/dns/v2/zone_transfer.py
@@ -0,0 +1,72 @@
+# Licensed 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.
+from openstack import resource
+
+
+class ZoneTransferBase(resource.Resource):
+    """DNS Zone Transfer Request/Accept Base Resource"""
+
+    _query_mapping = resource.QueryParameters(
+        'status'
+    )
+
+    #: Properties
+    #: Timestamp when the resource was created
+    created_at = resource.Body('created_at')
+    #: Key that is used as part of the zone transfer accept process.
+    #: This is only shown to the creator, and must be communicated out of band.
+    key = resource.Body('key')
+    #: The project id which the zone belongs to
+    project_id = resource.Body('project_id')
+    #: Current status of the zone import
+    status = resource.Body('status')
+    #: Timestamp when the resource was last updated
+    updated_at = resource.Body('updated_at')
+    #: Version of the resource
+    version = resource.Body('version', type=int)
+    #: ID for the zone that is being exported
+    zone_id = resource.Body('zone_id')
+
+
+class ZoneTransferRequest(ZoneTransferBase):
+    """DNS Zone Transfer Request Resource"""
+    base_path = '/zones/tasks/transfer_requests'
+    resources_key = 'transfer_requests'
+
+    # capabilities
+    allow_create = True
+    allow_fetch = True
+    allow_delete = True
+    allow_list = True
+    allow_commit = True
+
+    #: Description
+    description = resource.Body('description')
+    #: A project ID that the request will be limited to.
+    #: No other project will be allowed to accept this request.
+    target_project_id = resource.Body('target_project_id')
+    #: Name for the zone that is being exported
+    zone_name = resource.Body('zone_name')
+
+
+class ZoneTransferAccept(ZoneTransferBase):
+    """DNS Zone Transfer Accept Resource"""
+    base_path = '/zones/tasks/transfer_accepts'
+    resources_key = 'transfer_accepts'
+
+    # capabilities
+    allow_create = True
+    allow_fetch = True
+    allow_list = True
+
+    #: Name for the zone that is being exported
+    zone_transfer_request_id = resource.Body('zone_transfer_request_id')
diff --git a/openstack/tests/functional/dns/__init__.py b/openstack/tests/functional/dns/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/openstack/tests/functional/dns/v2/__init__.py b/openstack/tests/functional/dns/v2/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/openstack/tests/functional/dns/v2/test_zone.py b/openstack/tests/functional/dns/v2/test_zone.py
new file mode 100644
index 000000000..2d4527cc8
--- /dev/null
+++ b/openstack/tests/functional/dns/v2/test_zone.py
@@ -0,0 +1,52 @@
+# Licensed 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 random
+
+from openstack import connection
+from openstack.tests.functional import base
+
+
+class TestZone(base.BaseFunctionalTest):
+
+    def setUp(self):
+        super(TestZone, self).setUp()
+        self.require_service('dns')
+
+        self.conn = connection.from_config(cloud_name=base.TEST_CLOUD_NAME)
+
+        # Note: zone deletion is not an immediate operation, so each time
+        # chose a new zone name for a test
+        # getUniqueString is not guaranteed to return unique string between
+        # different tests of the same class.
+        self.ZONE_NAME = 'example-{0}.org.'.format(random.randint(1, 100))
+
+        self.zone = self.conn.dns.create_zone(
+            name=self.ZONE_NAME,
+            email='joe@example.org',
+            type='PRIMARY',
+            ttl=7200,
+            description='example zone'
+        )
+        self.addCleanup(self.conn.dns.delete_zone, self.zone)
+
+    def tearDown(self):
+        if self.zone:
+            self.conn.dns.delete_zone(self.zone)
+        super(TestZone, self).tearDown()
+
+    def test_get_zone(self):
+        zone = self.conn.dns.get_zone(self.zone)
+        self.assertEqual(self.zone, zone)
+
+    def test_list_zones(self):
+        names = [f.name for f in self.conn.dns.zones()]
+        self.assertIn(self.ZONE_NAME, names)
diff --git a/openstack/tests/unit/dns/__init__.py b/openstack/tests/unit/dns/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/openstack/tests/unit/dns/v2/__init__.py b/openstack/tests/unit/dns/v2/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/openstack/tests/unit/dns/v2/test_floating_ip.py b/openstack/tests/unit/dns/v2/test_floating_ip.py
new file mode 100644
index 000000000..412e453c9
--- /dev/null
+++ b/openstack/tests/unit/dns/v2/test_floating_ip.py
@@ -0,0 +1,56 @@
+# Licensed 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.
+
+from openstack.tests.unit import base
+
+from openstack.dns.v2 import floating_ip as fip
+
+
+IDENTIFIER = 'RegionOne:id'
+EXAMPLE = {
+    'status': 'PENDING',
+    'ptrdname': 'smtp.example.com.',
+    'description': 'This is a floating ip for 127.0.0.1',
+    'links': {
+        'self': 'dummylink/reverse/floatingips/RegionOne:id'
+    },
+    'ttl': 600,
+    'address': '172.24.4.10',
+    'action': 'CREATE',
+    'id': IDENTIFIER
+}
+
+
+class TestFloatingIP(base.TestCase):
+
+    def test_basic(self):
+        sot = fip.FloatingIP()
+        self.assertEqual('', sot.resource_key)
+        self.assertEqual('floatingips', sot.resources_key)
+        self.assertEqual('/reverse/floatingips', sot.base_path)
+        self.assertTrue(sot.allow_list)
+        self.assertFalse(sot.allow_create)
+        self.assertTrue(sot.allow_fetch)
+        self.assertTrue(sot.allow_commit)
+        self.assertFalse(sot.allow_delete)
+
+        self.assertEqual('PATCH', sot.commit_method)
+
+    def test_make_it(self):
+        sot = fip.FloatingIP(**EXAMPLE)
+        self.assertEqual(IDENTIFIER, sot.id)
+        self.assertEqual(EXAMPLE['ptrdname'], sot.ptrdname)
+        self.assertEqual(EXAMPLE['description'], sot.description)
+        self.assertEqual(EXAMPLE['ttl'], sot.ttl)
+        self.assertEqual(EXAMPLE['address'], sot.address)
+        self.assertEqual(EXAMPLE['action'], sot.action)
+        self.assertEqual(EXAMPLE['status'], sot.status)
diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py
new file mode 100644
index 000000000..9814ea59d
--- /dev/null
+++ b/openstack/tests/unit/dns/v2/test_proxy.py
@@ -0,0 +1,197 @@
+# Licensed 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.
+
+from openstack.dns.v2 import _proxy
+from openstack.dns.v2 import zone
+from openstack.dns.v2 import zone_import
+from openstack.dns.v2 import zone_export
+from openstack.dns.v2 import zone_transfer
+from openstack.dns.v2 import recordset
+from openstack.dns.v2 import floating_ip
+from openstack.tests.unit import test_proxy_base
+
+
+class TestDnsProxy(test_proxy_base.TestProxyBase):
+    def setUp(self):
+        super(TestDnsProxy, self).setUp()
+        self.proxy = _proxy.Proxy(self.session)
+
+
+class TestDnsZone(TestDnsProxy):
+    def test_zone_create(self):
+        self.verify_create(self.proxy.create_zone, zone.Zone,
+                           method_kwargs={'name': 'id'},
+                           expected_kwargs={'name': 'id',
+                                            'prepend_key': False})
+
+    def test_zone_delete(self):
+        self.verify_delete(self.proxy.delete_zone,
+                           zone.Zone, True)
+
+    def test_zone_find(self):
+        self.verify_find(self.proxy.find_zone, zone.Zone)
+
+    def test_zone_get(self):
+        self.verify_get(self.proxy.get_zone, zone.Zone)
+
+    def test_zones(self):
+        self.verify_list(self.proxy.zones, zone.Zone)
+
+    def test_zone_update(self):
+        self.verify_update(self.proxy.update_zone, zone.Zone)
+
+    def test_zone_abandon(self):
+        self._verify("openstack.dns.v2.zone.Zone.abandon",
+                     self.proxy.abandon_zone,
+                     method_args=[{'zone': 'id'}])
+
+    def test_zone_xfr(self):
+        self._verify("openstack.dns.v2.zone.Zone.xfr",
+                     self.proxy.xfr_zone,
+                     method_args=[{'zone': 'id'}])
+
+
+class TestDnsRecordset(TestDnsProxy):
+    def test_recordset_create(self):
+        self.verify_create(self.proxy.create_recordset, recordset.Recordset,
+                           method_kwargs={'zone': 'id'},
+                           expected_kwargs={'zone_id': 'id',
+                                            'prepend_key': False})
+
+    def test_recordset_delete(self):
+        self.verify_delete(self.proxy.delete_recordset,
+                           recordset.Recordset, True)
+
+    def test_recordset_update(self):
+        self.verify_update(self.proxy.update_recordset, recordset.Recordset)
+
+    def test_recordset_get(self):
+        self.verify_get(self.proxy.get_recordset, recordset.Recordset,
+                        method_kwargs={'zone': 'zid'},
+                        expected_kwargs={'zone_id': 'zid'}
+                        )
+
+    def test_recordsets(self):
+        self.verify_list(self.proxy.recordsets, recordset.Recordset,
+                         base_path='/recordsets')
+
+    def test_recordsets_zone(self):
+        self.verify_list(self.proxy.recordsets, recordset.Recordset,
+                         method_kwargs={'zone': 'zid'},
+                         expected_kwargs={'zone_id': 'zid'})
+
+
+class TestDnsFloatIP(TestDnsProxy):
+    def test_floating_ips(self):
+        self.verify_list(self.proxy.floating_ips, floating_ip.FloatingIP)
+
+    def test_floating_ip_get(self):
+        self.verify_get(self.proxy.get_floating_ip, floating_ip.FloatingIP)
+
+    def test_floating_ip_update(self):
+        self.verify_update(self.proxy.update_floating_ip,
+                           floating_ip.FloatingIP)
+
+    def test_zone_create(self):
+        self.verify_create(self.proxy.create_zone, zone.Zone,
+                           method_kwargs={'name': 'id'},
+                           expected_kwargs={'name': 'id',
+                                            'prepend_key': False})
+
+
+class TestDnsZoneImport(TestDnsProxy):
+    def test_zone_import_delete(self):
+        self.verify_delete(self.proxy.delete_zone_import,
+                           zone_import.ZoneImport, True)
+
+    def test_zone_import_get(self):
+        self.verify_get(self.proxy.get_zone_import, zone_import.ZoneImport)
+
+    def test_zone_imports(self):
+        self.verify_list(self.proxy.zone_imports, zone_import.ZoneImport)
+
+    def test_zone_import_create(self):
+        self.verify_create(self.proxy.create_zone_import,
+                           zone_import.ZoneImport,
+                           method_kwargs={'name': 'id'},
+                           expected_kwargs={'name': 'id',
+                                            'prepend_key': False})
+
+
+class TestDnsZoneExport(TestDnsProxy):
+    def test_zone_export_delete(self):
+        self.verify_delete(self.proxy.delete_zone_export,
+                           zone_export.ZoneExport, True)
+
+    def test_zone_export_get(self):
+        self.verify_get(self.proxy.get_zone_export, zone_export.ZoneExport)
+
+    def test_zone_export_get_text(self):
+        self.verify_get(self.proxy.get_zone_export_text,
+                        zone_export.ZoneExport,
+                        value=[{'id': 'zone_export_id_value'}],
+                        expected_kwargs={
+                            'base_path': '/zones/tasks/export/%(id)s/export'
+                        })
+
+    def test_zone_exports(self):
+        self.verify_list(self.proxy.zone_exports, zone_export.ZoneExport)
+
+    def test_zone_export_create(self):
+        self.verify_create(self.proxy.create_zone_export,
+                           zone_export.ZoneExport,
+                           method_args=[{'id': 'zone_id_value'}],
+                           method_kwargs={'name': 'id'},
+                           expected_kwargs={'name': 'id',
+                                            'zone_id': 'zone_id_value',
+                                            'prepend_key': False})
+
+
+class TestDnsZoneTransferRequest(TestDnsProxy):
+    def test_zone_transfer_request_delete(self):
+        self.verify_delete(self.proxy.delete_zone_transfer_request,
+                           zone_transfer.ZoneTransferRequest, True)
+
+    def test_zone_transfer_request_get(self):
+        self.verify_get(self.proxy.get_zone_transfer_request,
+                        zone_transfer.ZoneTransferRequest)
+
+    def test_zone_transfer_requests(self):
+        self.verify_list(self.proxy.zone_transfer_requests,
+                         zone_transfer.ZoneTransferRequest)
+
+    def test_zone_transfer_request_create(self):
+        self.verify_create(self.proxy.create_zone_transfer_request,
+                           zone_transfer.ZoneTransferRequest,
+                           method_args=[{'id': 'zone_id_value'}],
+                           method_kwargs={'name': 'id'},
+                           expected_kwargs={'name': 'id',
+                                            'zone_id': 'zone_id_value',
+                                            'prepend_key': False})
+
+    def test_zone_transfer_request_update(self):
+        self.verify_update(self.proxy.update_zone_transfer_request,
+                           zone_transfer.ZoneTransferRequest)
+
+
+class TestDnsZoneTransferAccept(TestDnsProxy):
+    def test_zone_transfer_accept_get(self):
+        self.verify_get(self.proxy.get_zone_transfer_accept,
+                        zone_transfer.ZoneTransferAccept)
+
+    def test_zone_transfer_accepts(self):
+        self.verify_list(self.proxy.zone_transfer_accepts,
+                         zone_transfer.ZoneTransferAccept)
+
+    def test_zone_transfer_accept_create(self):
+        self.verify_create(self.proxy.create_zone_transfer_accept,
+                           zone_transfer.ZoneTransferAccept)
diff --git a/openstack/tests/unit/dns/v2/test_recordset.py b/openstack/tests/unit/dns/v2/test_recordset.py
new file mode 100644
index 000000000..700a5148a
--- /dev/null
+++ b/openstack/tests/unit/dns/v2/test_recordset.py
@@ -0,0 +1,69 @@
+# Licensed 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.
+
+from openstack.tests.unit import base
+
+from openstack.dns.v2 import recordset
+
+
+IDENTIFIER = 'NAME'
+EXAMPLE = {
+    'description': 'This is an example record set.',
+    'updated_at': None,
+    'records': [
+        '10.1.0.2'
+    ],
+    'ttl': 3600,
+    'id': IDENTIFIER,
+    'name': 'example.org.',
+    'project_id': '4335d1f0-f793-11e2-b778-0800200c9a66',
+    'zone_id': '2150b1bf-dee2-4221-9d85-11f7886fb15f',
+    'zone_name': 'example.com.',
+    'created_at': '2014-10-24T19:59:44.000000',
+    'version': 1,
+    'type': 'A',
+    'status': 'ACTIVE',
+    'action': 'NONE'
+}
+
+
+class TestRecordset(base.TestCase):
+
+    def test_basic(self):
+        sot = recordset.Recordset()
+        self.assertEqual('recordset', sot.resource_key)
+        self.assertEqual('recordsets', sot.resources_key)
+        self.assertEqual('/zones/%(zone_id)s/recordsets', sot.base_path)
+        self.assertTrue(sot.allow_list)
+        self.assertTrue(sot.allow_create)
+        self.assertTrue(sot.allow_fetch)
+        self.assertTrue(sot.allow_commit)
+        self.assertTrue(sot.allow_delete)
+
+        self.assertDictEqual({'data': 'data',
+                              'description': 'description',
+                              'limit': 'limit',
+                              'marker': 'marker',
+                              'name': 'name',
+                              'status': 'status',
+                              'ttl': 'ttl',
+                              'type': 'type'},
+                             sot._query_mapping._mapping)
+
+    def test_make_it(self):
+        sot = recordset.Recordset(**EXAMPLE)
+        self.assertEqual(IDENTIFIER, sot.id)
+        self.assertEqual(EXAMPLE['description'], sot.description)
+        self.assertEqual(EXAMPLE['ttl'], sot.ttl)
+        self.assertEqual(EXAMPLE['type'], sot.type)
+        self.assertEqual(EXAMPLE['name'], sot.name)
+        self.assertEqual(EXAMPLE['status'], sot.status)
diff --git a/openstack/tests/unit/dns/v2/test_zone.py b/openstack/tests/unit/dns/v2/test_zone.py
new file mode 100644
index 000000000..5b2daafdf
--- /dev/null
+++ b/openstack/tests/unit/dns/v2/test_zone.py
@@ -0,0 +1,87 @@
+# Licensed 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.
+
+from keystoneauth1 import adapter
+import mock
+
+from openstack.tests.unit import base
+
+from openstack.dns.v2 import zone
+
+
+IDENTIFIER = 'NAME'
+EXAMPLE = {
+    'attributes': {
+        'tier': 'gold', 'ha': 'true'
+    },
+    'id': IDENTIFIER,
+    'name': 'test.org',
+    'email': 'joe@example.org',
+    'type': 'PRIMARY',
+    'ttl': 7200,
+    'description': 'This is an example zone.',
+    'status': 'ACTIVE'
+}
+
+
+class TestZone(base.TestCase):
+
+    def setUp(self):
+        super(TestZone, self).setUp()
+        self.resp = mock.Mock()
+        self.resp.body = None
+        self.resp.json = mock.Mock(return_value=self.resp.body)
+        self.resp.status_code = 200
+        self.sess = mock.Mock(spec=adapter.Adapter)
+        self.sess.post = mock.Mock(return_value=self.resp)
+        self.sess.default_microversion = None
+
+    def test_basic(self):
+        sot = zone.Zone()
+        self.assertEqual(None, sot.resource_key)
+        self.assertEqual('zones', sot.resources_key)
+        self.assertEqual('/zones', sot.base_path)
+        self.assertTrue(sot.allow_list)
+        self.assertTrue(sot.allow_create)
+        self.assertTrue(sot.allow_fetch)
+        self.assertTrue(sot.allow_commit)
+        self.assertTrue(sot.allow_delete)
+
+        self.assertEqual('PATCH', sot.commit_method)
+
+        self.assertDictEqual({'description': 'description',
+                              'email': 'email',
+                              'limit': 'limit',
+                              'marker': 'marker',
+                              'name': 'name',
+                              'status': 'status',
+                              'ttl': 'ttl',
+                              'type': 'type'},
+                             sot._query_mapping._mapping)
+
+    def test_make_it(self):
+        sot = zone.Zone(**EXAMPLE)
+        self.assertEqual(IDENTIFIER, sot.id)
+        self.assertEqual(EXAMPLE['email'], sot.email)
+        self.assertEqual(EXAMPLE['description'], sot.description)
+        self.assertEqual(EXAMPLE['ttl'], sot.ttl)
+        self.assertEqual(EXAMPLE['type'], sot.type)
+        self.assertEqual(EXAMPLE['name'], sot.name)
+        self.assertEqual(EXAMPLE['status'], sot.status)
+
+    def test_abandon(self):
+        sot = zone.Zone(**EXAMPLE)
+        self.assertIsNone(sot.abandon(self.sess))
+        self.sess.post.assert_called_with(
+            'zones/NAME/tasks/abandon',
+            json=None
+        )
diff --git a/openstack/tests/unit/dns/v2/test_zone_export.py b/openstack/tests/unit/dns/v2/test_zone_export.py
new file mode 100644
index 000000000..5b7876298
--- /dev/null
+++ b/openstack/tests/unit/dns/v2/test_zone_export.py
@@ -0,0 +1,80 @@
+# Licensed 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 mock
+from keystoneauth1 import adapter
+from openstack.tests.unit import base
+
+from openstack.dns.v2 import zone_export
+
+
+IDENTIFIER = '074e805e-fe87-4cbb-b10b-21a06e215d41'
+EXAMPLE = {
+    'status': 'COMPLETE',
+    'zone_id': '6625198b-d67d-47dc-8d29-f90bd60f3ac4',
+    'links': {
+        'self': 'http://127.0.0.1:9001/v2/zones/tasks/exports/074e805e-f',
+        'href': 'http://127.0.0.1:9001/v2/zones/6625198b-d67d-'
+    },
+    'created_at': '2015-05-08T15:43:42.000000',
+    'updated_at': '2015-05-08T15:43:43.000000',
+    'version': 2,
+    'location': 'designate://v2/zones/tasks/exports/8ec17fe1/export',
+    'message': 'example.com. exported',
+    'project_id': 'noauth-project',
+    'id': IDENTIFIER
+}
+
+
+@mock.patch.object(zone_export.ZoneExport, '_translate_response', mock.Mock())
+class TestZoneExport(base.TestCase):
+
+    def test_basic(self):
+        sot = zone_export.ZoneExport()
+        self.assertEqual('', sot.resource_key)
+        self.assertEqual('exports', sot.resources_key)
+        self.assertEqual('/zones/tasks/export', sot.base_path)
+        self.assertTrue(sot.allow_list)
+        self.assertTrue(sot.allow_create)
+        self.assertTrue(sot.allow_fetch)
+        self.assertFalse(sot.allow_commit)
+        self.assertTrue(sot.allow_delete)
+
+        self.assertDictEqual({'limit': 'limit',
+                              'marker': 'marker',
+                              'message': 'message',
+                              'status': 'status',
+                              'zone_id': 'zone_id'},
+                             sot._query_mapping._mapping)
+
+    def test_make_it(self):
+        sot = zone_export.ZoneExport(**EXAMPLE)
+        self.assertEqual(IDENTIFIER, sot.id)
+        self.assertEqual(EXAMPLE['created_at'], sot.created_at)
+        self.assertEqual(EXAMPLE['updated_at'], sot.updated_at)
+        self.assertEqual(EXAMPLE['version'], sot.version)
+        self.assertEqual(EXAMPLE['message'], sot.message)
+        self.assertEqual(EXAMPLE['project_id'], sot.project_id)
+        self.assertEqual(EXAMPLE['status'], sot.status)
+        self.assertEqual(EXAMPLE['zone_id'], sot.zone_id)
+
+    def test_create(self):
+        sot = zone_export.ZoneExport()
+        response = mock.Mock()
+        response.json = mock.Mock(return_value='')
+        self.session = mock.Mock(spec=adapter.Adapter)
+        self.session.default_microversion = '1.1'
+
+        sot.create(self.session)
+        self.session.post.assert_called_once_with(
+            mock.ANY, json=None,
+            headers=None,
+            microversion=self.session.default_microversion)
diff --git a/openstack/tests/unit/dns/v2/test_zone_import.py b/openstack/tests/unit/dns/v2/test_zone_import.py
new file mode 100644
index 000000000..808830338
--- /dev/null
+++ b/openstack/tests/unit/dns/v2/test_zone_import.py
@@ -0,0 +1,79 @@
+# Licensed 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 mock
+from keystoneauth1 import adapter
+from openstack.tests.unit import base
+
+from openstack.dns.v2 import zone_import
+
+
+IDENTIFIER = '074e805e-fe87-4cbb-b10b-21a06e215d41'
+EXAMPLE = {
+    'status': 'COMPLETE',
+    'zone_id': '6625198b-d67d-47dc-8d29-f90bd60f3ac4',
+    'links': {
+        'self': 'http://127.0.0.1:9001/v2/zones/tasks/imports/074e805e-f',
+        'href': 'http://127.0.0.1:9001/v2/zones/6625198b-d67d-'
+    },
+    'created_at': '2015-05-08T15:43:42.000000',
+    'updated_at': '2015-05-08T15:43:43.000000',
+    'version': 2,
+    'message': 'example.com. imported',
+    'project_id': 'noauth-project',
+    'id': IDENTIFIER
+}
+
+
+@mock.patch.object(zone_import.ZoneImport, '_translate_response', mock.Mock())
+class TestZoneImport(base.TestCase):
+
+    def test_basic(self):
+        sot = zone_import.ZoneImport()
+        self.assertEqual('', sot.resource_key)
+        self.assertEqual('imports', sot.resources_key)
+        self.assertEqual('/zones/tasks/import', sot.base_path)
+        self.assertTrue(sot.allow_list)
+        self.assertTrue(sot.allow_create)
+        self.assertTrue(sot.allow_fetch)
+        self.assertFalse(sot.allow_commit)
+        self.assertTrue(sot.allow_delete)
+
+        self.assertDictEqual({'limit': 'limit',
+                              'marker': 'marker',
+                              'message': 'message',
+                              'status': 'status',
+                              'zone_id': 'zone_id'},
+                             sot._query_mapping._mapping)
+
+    def test_make_it(self):
+        sot = zone_import.ZoneImport(**EXAMPLE)
+        self.assertEqual(IDENTIFIER, sot.id)
+        self.assertEqual(EXAMPLE['created_at'], sot.created_at)
+        self.assertEqual(EXAMPLE['updated_at'], sot.updated_at)
+        self.assertEqual(EXAMPLE['version'], sot.version)
+        self.assertEqual(EXAMPLE['message'], sot.message)
+        self.assertEqual(EXAMPLE['project_id'], sot.project_id)
+        self.assertEqual(EXAMPLE['status'], sot.status)
+        self.assertEqual(EXAMPLE['zone_id'], sot.zone_id)
+
+    def test_create(self):
+        sot = zone_import.ZoneImport()
+        response = mock.Mock()
+        response.json = mock.Mock(return_value='')
+        self.session = mock.Mock(spec=adapter.Adapter)
+        self.session.default_microversion = '1.1'
+
+        sot.create(self.session)
+        self.session.post.assert_called_once_with(
+            mock.ANY, json=None,
+            headers={'content-type': 'text/dns'},
+            microversion=self.session.default_microversion)
diff --git a/openstack/tests/unit/dns/v2/test_zone_transfer.py b/openstack/tests/unit/dns/v2/test_zone_transfer.py
new file mode 100644
index 000000000..7064834fe
--- /dev/null
+++ b/openstack/tests/unit/dns/v2/test_zone_transfer.py
@@ -0,0 +1,103 @@
+# Licensed 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.
+from openstack.tests.unit import base
+
+from openstack.dns.v2 import zone_transfer
+
+
+IDENTIFIER = '074e805e-fe87-4cbb-b10b-21a06e215d41'
+EXAMPLE_REQUEST = {
+    'created_at': '2014-07-17T20:34:40.882579',
+    'description': 'some description',
+    'id': IDENTIFIER,
+    'key': '9Z2R50Y0',
+    'project_id': '1',
+    'status': 'ACTIVE',
+    'target_project_id': '123456',
+    'updated_at': None,
+    'zone_id': '6b78734a-aef1-45cd-9708-8eb3c2d26ff8',
+    'zone_name': 'qa.dev.example.com.',
+}
+EXAMPLE_ACCEPT = {
+    'status': 'COMPLETE',
+    'zone_id': 'b4542f5a-f1ea-4ec1-b850-52db9dc3f465',
+    'created_at': '2016-06-22 06:13:55',
+    'updated_at': 'null',
+    'key': 'FUGXMZ5N',
+    'project_id': '2e43de7ce3504a8fb90a45382532c37e',
+    'id': IDENTIFIER,
+    'zone_transfer_request_id': '794fdf58-6e1d-41da-8b2d-16b6d10c8827'
+}
+
+
+class TestZoneTransferRequest(base.TestCase):
+
+    def test_basic(self):
+        sot = zone_transfer.ZoneTransferRequest()
+        # self.assertEqual('', sot.resource_key)
+        self.assertEqual('transfer_requests', sot.resources_key)
+        self.assertEqual('/zones/tasks/transfer_requests', sot.base_path)
+        self.assertTrue(sot.allow_list)
+        self.assertTrue(sot.allow_create)
+        self.assertTrue(sot.allow_fetch)
+        self.assertTrue(sot.allow_commit)
+        self.assertTrue(sot.allow_delete)
+
+        self.assertDictEqual({'limit': 'limit',
+                              'marker': 'marker',
+                              'status': 'status'},
+                             sot._query_mapping._mapping)
+
+    def test_make_it(self):
+        sot = zone_transfer.ZoneTransferRequest(**EXAMPLE_REQUEST)
+        self.assertEqual(IDENTIFIER, sot.id)
+        self.assertEqual(EXAMPLE_REQUEST['created_at'], sot.created_at)
+        self.assertEqual(EXAMPLE_REQUEST['updated_at'], sot.updated_at)
+        self.assertEqual(EXAMPLE_REQUEST['description'], sot.description)
+        self.assertEqual(EXAMPLE_REQUEST['key'], sot.key)
+        self.assertEqual(EXAMPLE_REQUEST['project_id'], sot.project_id)
+        self.assertEqual(EXAMPLE_REQUEST['status'], sot.status)
+        self.assertEqual(EXAMPLE_REQUEST['target_project_id'],
+                         sot.target_project_id)
+        self.assertEqual(EXAMPLE_REQUEST['zone_id'], sot.zone_id)
+        self.assertEqual(EXAMPLE_REQUEST['zone_name'], sot.zone_name)
+
+
+class TestZoneTransferAccept(base.TestCase):
+
+    def test_basic(self):
+        sot = zone_transfer.ZoneTransferAccept()
+        # self.assertEqual('', sot.resource_key)
+        self.assertEqual('transfer_accepts', sot.resources_key)
+        self.assertEqual('/zones/tasks/transfer_accepts', sot.base_path)
+        self.assertTrue(sot.allow_list)
+        self.assertTrue(sot.allow_create)
+        self.assertTrue(sot.allow_fetch)
+        self.assertFalse(sot.allow_commit)
+        self.assertFalse(sot.allow_delete)
+
+        self.assertDictEqual({'limit': 'limit',
+                              'marker': 'marker',
+                              'status': 'status'},
+                             sot._query_mapping._mapping)
+
+    def test_make_it(self):
+        sot = zone_transfer.ZoneTransferAccept(**EXAMPLE_ACCEPT)
+        self.assertEqual(IDENTIFIER, sot.id)
+        self.assertEqual(EXAMPLE_ACCEPT['created_at'], sot.created_at)
+        self.assertEqual(EXAMPLE_ACCEPT['updated_at'], sot.updated_at)
+        self.assertEqual(EXAMPLE_ACCEPT['key'], sot.key)
+        self.assertEqual(EXAMPLE_ACCEPT['project_id'], sot.project_id)
+        self.assertEqual(EXAMPLE_ACCEPT['status'], sot.status)
+        self.assertEqual(EXAMPLE_ACCEPT['zone_id'], sot.zone_id)
+        self.assertEqual(EXAMPLE_ACCEPT['zone_transfer_request_id'],
+                         sot.zone_transfer_request_id)
diff --git a/openstack/tests/unit/test_proxy_base.py b/openstack/tests/unit/test_proxy_base.py
index 7b4bf6cdc..e8e1eb834 100644
--- a/openstack/tests/unit/test_proxy_base.py
+++ b/openstack/tests/unit/test_proxy_base.py
@@ -207,6 +207,7 @@ class TestProxyBase(base.TestCase):
         if 'paginated' in kwargs:
             expected_kwargs.update({"paginated": kwargs.pop('paginated')})
         method_kwargs = kwargs.pop("method_kwargs", {})
+        expected_kwargs["base_path"] = kwargs.pop("base_path", None)
         self._verify2(mock_method, test_method,
                       method_kwargs=method_kwargs,
                       expected_args=[resource_type],
diff --git a/releasenotes/notes/add-dns-606cc018e01d40fa.yaml b/releasenotes/notes/add-dns-606cc018e01d40fa.yaml
new file mode 100644
index 000000000..dcaab35dc
--- /dev/null
+++ b/releasenotes/notes/add-dns-606cc018e01d40fa.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    Adds support for `dns
+    <https://developer.openstack.org/api-ref/dns/>`_ service.