DNS Support for Instance IP.

Can be turned off using a flag.
Provides a Rackspace DNS driver.
Provides an interface to write drivers for other DNS engines.
This commit is contained in:
Sudarshan Acharya 2012-05-18 15:49:12 -05:00
parent 758dce6dd8
commit 0c34c75dc4
22 changed files with 1132 additions and 0 deletions

View File

@ -49,6 +49,9 @@ reddwarf_volume_support = True
device_path = /dev/vdb
mount_point = /var/lib/mysql
# Reddwarf DNS
reddwarf_dns_support = False
# ============ notifer queue kombu connection options ========================
notifier_queue_hostname = localhost

View File

@ -70,6 +70,11 @@ class ComputeInstanceNotFound(NotFound):
message = _("Resource %(instance_id)s can not be retrieved.")
class DnsRecordNotFound(NotFound):
message = _("DnsRecord with name= %(name)s not found.")
class OverLimit(ReddwarfError):

View File

@ -22,6 +22,11 @@ from novaclient.v1_1.client import Client
CONFIG = config.Config
def create_dns_client(context):
from reddwarf.dns.manager import DnsManager
return DnsManager()
def create_guest_client(context, id):
from reddwarf.guestagent.api import API
return API(context, id)

View File

@ -34,6 +34,8 @@ def map(engine, models):
Table('service_images', meta, autoload=True))
orm.mapper(models['service_statuses'],
Table('service_statuses', meta, autoload=True))
orm.mapper(models['rsdns_records'],
Table('rsdns_records', meta, autoload=True))
def mapping_exists(model):

View File

@ -37,6 +37,7 @@ instances = Table('instances', meta,
Column('created', DateTime()),
Column('updated', DateTime()),
Column('name', String(255)),
Column('hostname', String(255)),
Column('compute_instance_id', String(36)),
Column('task_id', Integer()),
Column('task_description', String(32)),

View File

@ -0,0 +1,44 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# 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 sqlalchemy import ForeignKey
from sqlalchemy.schema import Column
from sqlalchemy.schema import MetaData
from sqlalchemy.schema import UniqueConstraint
from reddwarf.db.sqlalchemy.migrate_repo.schema import Table
from reddwarf.db.sqlalchemy.migrate_repo.schema import create_tables
from reddwarf.db.sqlalchemy.migrate_repo.schema import drop_tables
from reddwarf.db.sqlalchemy.migrate_repo.schema import String
meta = MetaData()
rsdns_records = Table('rsdns_records', meta,
Column('name', String(length=255), primary_key=True),
Column('record_id', String(length=64)))
def upgrade(migrate_engine):
meta.bind = migrate_engine
create_tables([rsdns_records])
def downgrade(migrate_engine):
meta.bind = migrate_engine
drop_tables([rsdns_records])

View File

@ -40,10 +40,12 @@ def configure_db(options, models_mapper=None):
models_mapper.map(_ENGINE)
else:
from reddwarf.instance import models as base_models
from reddwarf.dns.rsdns import models as dns_models
from reddwarf.extensions.mysql import models as mysql_models
model_modules = [
base_models,
dns_models,
mysql_models,
]

18
reddwarf/dns/__init__.py Normal file
View File

@ -0,0 +1,18 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2011 Openstack, LLC.
# All Rights Reserved.
#
# 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 manager import DnsManager

117
reddwarf/dns/driver.py Normal file
View File

@ -0,0 +1,117 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2011 Openstack, LLC.
# All Rights Reserved.
#
# 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.
"""
Dns Driver base class that all DNS drivers should inherit from
"""
class DnsDriver(object):
"""The base class that all Dns drivers should inherit from."""
def __init__(self):
pass
def create_entry(self, entry):
"""Creates the entry in the driver at the given dns zone."""
pass
def delete_entry(self, name, type, dns_zone=None):
"""Deletes an entry with the given name and type from a dns zone."""
pass
def get_entries_by_content(self, content, dns_zone=None):
"""Retrieves all entries in a dns_zone with a matching content field"""
pass
def get_entries_by_name(self, name, dns_zone=None):
"""Retrieves all entries in a dns zone with the given name field."""
pass
def get_dns_zones(self, name=None):
"""Returns all dns zones (optionally filtered by the name argument."""
pass
def modify_content(self, name, content, dns_zone):
#TODO(tim.simpson) I've found no use for this in RS impl of DNS w/
# instances. Check to see its really needed.
pass
def rename_entry(self, content, name, dns_zone):
#TODO(tim.simpson) I've found no use for this in RS impl of DNS w/
# instances. Check to see its really needed.
pass
class DnsInstanceEntryFactory(object):
"""Defines how instance DNS entries are created for instances.
By default, the DNS entry returns None meaning instances do not get entries
associated with them. Override the create_entry method to change this
behavior.
"""
def create_entry(self, instance):
return None
class DnsSimpleInstanceEntryFactory(object):
"""Creates a CNAME with the name being the instance name."""
def create_entry(self, instance):
return DnsEntry(name=instance.name, content=None, type="CNAME")
class DnsEntry(object):
"""Simple representation of a DNS record."""
def __init__(self, name, content, type, ttl=None, priority=None,
dns_zone=None):
self.content = content
self.name = name
self.type = type
self.priority = priority
self.dns_zone = dns_zone
self.ttl = ttl
def __repr__(self):
return 'DnsEntry(name="%s", content="%s", type="%s", ' \
'ttl=%s, priority=%s, dns_zone=%s)' % (self.name, self.content,
self.type, self.ttl, self.priority, self.dns_zone)
def __str__(self):
return "{ name:%s, content:%s, type:%s, zone:%s }" % \
(self.name, self.content, self.type, self.dns_zone)
class DnsZone(object):
"""Represents a DNS Zone.
For some APIs it is inefficient to simply represent a zone as a string
because this would necessitate a look up on every call. So this opaque
object can contain additional data needed by the DNS driver. The only
constant is it must contain the domain name of the zone.
"""
@property
def name(self):
return ""
def __str__(self):
return self.name

85
reddwarf/dns/manager.py Normal file
View File

@ -0,0 +1,85 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2011 Openstack, LLC.
# All Rights Reserved.
#
# 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.
"""
Dns manager.
"""
import logging
from reddwarf.common import utils
from reddwarf.common import config
from reddwarf.dns.rsdns.driver import RsDnsInstanceEntryFactory
LOG = logging.getLogger(__name__)
class DnsManager(object):
"""Handles associating DNS to and from IPs."""
def __init__(self, dns_driver=None, dns_instance_entry_factory=None,
*args, **kwargs):
if not dns_driver:
dns_driver = config.Config.get("dns_driver",
"reddwarf.dns.driver.DnsDriver")
dns_driver = utils.import_object(dns_driver)
self.driver = dns_driver()
if not dns_instance_entry_factory:
dns_instance_entry_factory = config.Config.get(
'dns_instance_entry_factory',
'reddwarf.dns.driver.DnsInstanceEntryFactory')
entry_factory = utils.import_object(dns_instance_entry_factory)
self.entry_factory = entry_factory()
def create_instance_entry(self, instance_id, content):
"""Connects a new instance with a DNS entry.
:param instance_id: The reddwarf instance_id to associate.
:param content: The IP content attached to the instance.
"""
entry = self.entry_factory.create_entry(instance_id)
LOG.debug("Creating entry address %s." % str(entry))
if entry:
entry.content = content[0]
self.driver.create_entry(entry)
def delete_instance_entry(self, instance_id, content=None):
"""Removes a DNS entry associated to an instance.
:param instance_id: The reddwarf instance id to associate.
:param content: The IP content attached to the instance.
"""
entry = self.entry_factory.create_entry(instance_id)
LOG.debug("Deleting instance entry with %s" % str(entry))
if entry:
self.driver.delete_entry(entry.name, entry.type)
def update_hostname(self, instance):
"""
Create the hostname field based on the instance id.
Use instance by default
"""
dns_support = config.Config.get('reddwarf_dns_support', 'False')
if utils.bool_from_string(dns_support):
entry = self.entry_factory.create_entry(instance.id)
instance.hostname = entry.name
instance.save()
else:
instance.hostname = instance.name
instance.save()

View File

@ -0,0 +1,15 @@
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# 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.

View File

@ -0,0 +1,221 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2011 Openstack, LLC.
# All Rights Reserved.
#
# 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.
"""
Dns Driver that uses Rackspace DNSaaS.
"""
import hashlib
import logging
from reddwarf.common import config
from reddwarf.common import exception
from reddwarf.common.exception import NotFound
from reddwarf.dns.rsdns.models import RsDnsRecord
from rsdns.client import DNSaas
from rsdns.client.future import RsDnsError
from reddwarf.dns.driver import DnsEntry
DNS_HOSTNAME = config.Config.get("dns_hostname", "")
DNS_ACCOUNT_ID = config.Config.get("dns_account_id", 0)
DNS_AUTH_URL = config.Config.get("dns_auth_url", "")
DNS_DOMAIN_NAME = config.Config.get("dns_domain_name", "")
DNS_USERNAME = config.Config.get("dns_username", "")
DNS_PASSKEY = config.Config.get("dns_passkey", "")
DNS_MANAGEMENT_BASE_URL = config.Config.get("dns_management_base_url", "")
DNS_TTL = config.Config.get("dns_ttl", 300)
DNS_DOMAIN_ID = config.Config.get("dns_domain_id", 1)
LOG = logging.getLogger(__name__)
class EntryToRecordConverter(object):
def __init__(self, default_dns_zone):
self.default_dns_zone = default_dns_zone
def domain_to_dns_zone(self, domain):
return RsDnsZone(id=domain.id, name=domain.name)
def name_to_long_name(self, name, dns_zone=None):
dns_zone = dns_zone or self.default_dns_zone
if name:
long_name = name + "." + dns_zone.name
else:
long_name = ""
return long_name
def record_to_entry(self, record, dns_zone):
entry_name = record.name
return DnsEntry(name=entry_name, content=record.data, type=record.type,
ttl=record.ttl, dns_zone=dns_zone)
def create_client_with_flag_values():
"""Creates a RS DNSaaS client using the Flag values."""
if DNS_MANAGEMENT_BASE_URL == None:
raise RuntimeError("Missing flag value for dns_management_base_url.")
return DNSaas(DNS_ACCOUNT_ID, DNS_USERNAME, DNS_PASSKEY,
auth_url=DNS_AUTH_URL,
management_base_url=DNS_MANAGEMENT_BASE_URL)
def find_default_zone(dns_client, raise_if_zone_missing=True):
"""Using the domain_name from the FLAG values, creates a zone.
Because RS DNSaaS needs the ID, we need to find this value before we start.
In testing it's difficult to keep up with it because the database keeps
getting wiped... maybe later we could go back to storing it as a FLAG value
"""
domain_name = DNS_DOMAIN_NAME
try:
domains = dns_client.domains.list(name=domain_name)
for domain in domains:
if domain.name == domain_name:
return RsDnsZone(id=domain.id, name=domain_name)
except NotFound:
pass
if not raise_if_zone_missing:
return RsDnsZone(id=None, name=domain_name)
raise RuntimeError("The dns_domain_name from the FLAG values (%s) "
"does not exist! account_id=%s, username=%s, LIST=%s"
% (domain_name, DNS_ACCOUNT_ID, DNS_USERNAME, domains))
class RsDnsDriver(object):
"""Uses RS DNSaaS"""
def __init__(self, raise_if_zone_missing=True):
self.dns_client = create_client_with_flag_values()
self.dns_client.authenticate()
self.default_dns_zone = RsDnsZone(id=DNS_DOMAIN_ID,
name=DNS_DOMAIN_NAME)
self.converter = EntryToRecordConverter(self.default_dns_zone)
if DNS_TTL < 300:
raise Exception("TTL value '--dns_ttl=%s' should be greater than"\
" 300" % DNS_TTL)
def create_entry(self, entry):
dns_zone = entry.dns_zone or self.default_dns_zone
if dns_zone.id == None:
raise TypeError("The entry's dns_zone must have an ID specified.")
name = entry.name # + "." + dns_zone.name
LOG.debug("Going to create RSDNS entry %s." % name)
try:
future = self.dns_client.records.create(domain=dns_zone.id,
record_name=name,
record_data=entry.content,
record_type=entry.type,
record_ttl=entry.ttl)
try:
#poll_until(lambda : future.ready, sleep_time=2,
# time_out=60*2)
while(future.ready is None):
import time
time.sleep(2)
if len(future.resource) < 1:
raise RsDnsError("No DNS records were created.")
elif len(future.resource) > 1:
LOG.error("More than one DNS record created. Ignoring.")
actual_record = future.resource[0]
RsDnsRecord.create(name=name, record_id=actual_record.id)
LOG.debug("Added RS DNS entry.")
except RsDnsError as rde:
LOG.error("An error occurred creating DNS entry!")
raise
except Exception as ex:
LOG.error("Error when creating a DNS record!")
raise
def delete_entry(self, name, type, dns_zone=None):
dns_zone = dns_zone or self.default_dns_zone
long_name = name
db_record = RsDnsRecord.find_by(name=name)
record = self.dns_client.records.get(domain_id=dns_zone.id,
record_id=db_record.id)
if record.name != name or record.type != 'A':
LOG.error("Tried to delete DNS record with name=%s, id=%s, but the"
" database returned a DNS record with the name %s and "
"type %s." % (name, db_record.id, record.name,
record.type))
raise exception.DnsRecordNotFound(name)
self.dns_client.records.delete(domain_id=dns_zone.id,
record_id=record.id)
db_record.delete()
def get_entries(self, name=None, content=None, dns_zone=None):
dns_zone = dns_zone or self.defaucreate_entrylt_dns_zone
long_name = name # self.converter.name_to_long_name(name)
records = self.dns_client.records.list(domain_id=dns_zone.id,
record_name=long_name,
record_address=content)
return [self.converter.record_to_entry(record, dns_zone)
for record in records]
def get_entries_by_content(self, content, dns_zone=None):
return self.get_entries(content=content)
def get_entries_by_name(self, name, dns_zone=None):
return self.get_entries(name=name, dns_zone=dns_zone)
def get_dns_zones(self, name=None):
domains = self.dns_client.domains.list(name=name)
return [self.converter.domain_to_dns_zone(domain)
for domain in domains]
def modify_content(self, *args, **kwargs):
raise NotImplementedError("Not implemented for RS DNS.")
def rename_entry(self, *args, **kwargs):
raise NotImplementedError("Not implemented for RS DNS.")
class RsDnsInstanceEntryFactory(object):
"""Defines how instance DNS entries are created for instances."""
def __init__(self, dns_domain_id=None):
dns_domain_id = dns_domain_id or DNS_DOMAIN_ID
self.default_dns_zone = RsDnsZone(id=dns_domain_id,
name=DNS_DOMAIN_NAME)
def create_entry(self, instance_id):
id = instance_id
hostname = ("%s.%s" % (hashlib.sha1(id).hexdigest(),
self.default_dns_zone.name))
return DnsEntry(name=hostname, content=None, type="A",
ttl=DNS_TTL, dns_zone=self.default_dns_zone)
class RsDnsZone(object):
def __init__(self, id, name):
self.name = name
self.id = id
def __eq__(self, other):
return isinstance(other, RsDnsZone) and\
self.name == other.name and self.id == other.id
def __str__(self):
return "%s:%s" % (self.id, self.name)

View File

@ -0,0 +1,80 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010-2011 OpenStack LLC.
# All Rights Reserved.
#
# 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.
"""
Model classes that map instance Ip to dns record.
"""
import logging
from reddwarf import db
from reddwarf.common.models import ModelBase
from reddwarf.instance.models import InvalidModelError
from reddwarf.instance.models import ModelNotFoundError
LOG = logging.getLogger(__name__)
def persisted_models():
return {
'rsdns_records': RsDnsRecord,
}
class RsDnsRecord(ModelBase):
_data_fields = ['name', 'record_id']
_table_name = 'rsdns_records'
def __init__(self, name, record_id):
self.name = name
self.record_id = record_id
@classmethod
def create(cls, **values):
record = cls(**values).save()
if not record.is_valid():
raise InvalidModelError(record.errors)
return record
def save(self):
if not self.is_valid():
raise InvalidModelError(self.errors)
LOG.debug(_("Saving %s: %s") %
(self.__class__.__name__, self.__dict__))
return db.db_api.save(self)
def delete(self):
LOG.debug(_("Deleting %s: %s") %
(self.__class__.__name__, self.__dict__))
return db.db_api.delete(self)
@classmethod
def find_by(cls, **conditions):
model = cls.get_by(**conditions)
if model is None:
raise ModelNotFoundError(_("%s Not Found") % cls.__name__)
return model
@classmethod
def get_by(cls, **kwargs):
return db.db_api.find_by(cls, **cls._process_conditions(kwargs))
@classmethod
def _process_conditions(cls, raw_conditions):
"""Override in inheritors to format/modify any conditions."""
return raw_conditions

View File

@ -31,12 +31,14 @@ from reddwarf.instance.tasks import InstanceTask
from reddwarf.instance.tasks import InstanceTasks
from reddwarf.common.models import ModelBase
from novaclient import exceptions as nova_exceptions
from reddwarf.common.remote import create_dns_client
from reddwarf.common.remote import create_nova_client
from reddwarf.common.remote import create_nova_volume_client
from reddwarf.common.remote import create_guest_client
from eventlet import greenthread
from reddwarf.instance.views import get_ip_address
CONFIG = config.Config
@ -188,6 +190,12 @@ class Instance(object):
#TODO(tim.simpson): Put this in the task manager somehow to shepard
# deletion?
dns_support = config.Config.get("reddwarf_dns_support", 'False')
LOG.debug(_("reddwarf dns support = %s") % dns_support)
if utils.bool_from_string(dns_support):
dns_client = create_dns_client(self.context)
dns_client.delete_instance_entry(instance_id=self.db_info['id'])
def _delete_server(self):
try:
self.server.delete()
@ -299,6 +307,21 @@ class Instance(object):
guest.prepare(512, model_schemas, users=[],
device_path=device_path,
mount_point=mount_point)
dns_support = config.Config.get("reddwarf_dns_support", 'False')
LOG.debug(_("reddwarf dns support = %s") % dns_support)
if utils.bool_from_string(dns_support):
#TODO: Bring back our good friend poll_until.
while(server.addresses == {}):
import time
time.sleep(1)
server = client.servers.get(server.id)
LOG.debug("Waiting for address %s" % server.addresses)
dns_client = create_dns_client(context)
dns_client.update_hostname(db_info)
dns_client.create_instance_entry(db_info['id'],
get_ip_address(server.addresses))
return Instance(context, db_info, server, service_status, volumes)
def get_guest(self):

View File

@ -45,6 +45,7 @@ class InstanceView(object):
instance_dict = {
"id": self.instance.id,
"name": self.instance.name,
"hostname": self.instance.db_info.hostname,
"status": self.instance.status,
"links": self.instance.links
}

22
rsdns/__init__.py Normal file
View File

@ -0,0 +1,22 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2011 Openstack, LLC.
# All Rights Reserved.
#
# 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.
"""
Dns Driver that uses Rackspace DNSaaS.
"""
__all__ = ["client"]

24
rsdns/client/__init__.py Normal file
View File

@ -0,0 +1,24 @@
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# 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.
"""
dnsclient module.
"""
from rsdns.client.dns_client import DNSaas
from rsdns.client.dns_client import DNSaasClient
from rsdns.client.domains import DomainsManager
from rsdns.client.records import RecordsManager

122
rsdns/client/dns_client.py Normal file
View File

@ -0,0 +1,122 @@
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# 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.
"""
DNS Client interface. Child of OpenStack client to handle auth issues.
We have to duplicate a lot of code from the OpenStack client since so much
is different here.
"""
import logging
import exceptions
try:
import json
except ImportError:
import simplejson as json
from novaclient.client import HTTPClient
from novaclient.v1_1.client import Client
LOG = logging.getLogger('rsdns.client.dns_client')
class DNSaasClient(HTTPClient):
def __init__(self, accountId, user, apikey, auth_url, management_base_url):
tenant = "dbaas"
super(DNSaasClient, self).__init__(user, apikey, tenant, auth_url)
self.accountId = accountId
self.management_base_url = management_base_url
self.api_key = apikey
self.disable_ssl_certificate_validation = True
self.service = "cloudDNS"
def authenticate(self):
"""Set the management url and auth token"""
req_body = {'credentials':{'username':self.user, 'key':self.api_key}}
resp, body = self.request(self.auth_url, "POST", body=req_body)
if 'access' in body:
if not self.management_url:
# Assume the new Keystone lite:
catalog = body['access']['serviceCatalog']
for service in catalog:
if service['name'] == self.service:
self.management_url = service['adminURL']
self.auth_token = body['access']['token']['id']
else:
# Assume pre-Keystone Light:
try:
if not self.management_url:
keys = ['auth',
'serviceCatalog',
self.service,
0,
'publicURL']
url = body
for key in keys:
url = url[key]
self.management_url = url
self.auth_token = body['auth']['token']['id']
except KeyError:
raise NotImplementedError("Service: %s is not available"
% self.service)
def request(self, *args, **kwargs):
kwargs.setdefault('headers', kwargs.get('headers', {}))
kwargs['headers']['User-Agent'] = self.USER_AGENT
kwargs['headers']['Accept'] = 'application/json'
if 'body' in kwargs:
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['body'] = json.dumps(kwargs['body'])
LOG.debug("REQ HEADERS:" + str(kwargs['headers']))
LOG.debug("REQ BODY:" + str(kwargs['body']))
resp, body = super(HTTPClient, self).request(*args, **kwargs)
self.http_log(args, kwargs, resp, body)
if body:
try:
body = json.loads(body)
except ValueError:
pass
else:
body = None
if resp.status in (400, 401, 403, 404, 408, 409, 413, 500, 501):
raise exceptions.from_response(resp, body)
return resp, body
class DNSaas(Client):
"""
Top-level object to access the DNSaas service
"""
def __init__(self, accountId, username, apikey,
auth_url='https://auth.api.rackspacecloud.com/v1.0',
management_base_url=None):
from rsdns.client.dns_client import DNSaasClient
from rsdns.client.domains import DomainsManager
from rsdns.client.records import RecordsManager
super(DNSaas, self).__init__(self, accountId, username, apikey, auth_url, management_base_url)
self.client = DNSaasClient(accountId, username, apikey, auth_url,
management_base_url)
self.domains = DomainsManager(self)
self.records = RecordsManager(self)

89
rsdns/client/domains.py Normal file
View File

@ -0,0 +1,89 @@
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# 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.
"""
Domains interface.
"""
from novaclient import base
import os
from rsdns.client.future import FutureResource
class Domain(base.Resource):
"""
A Domain has a name and stores records. In the API they are id'd by ints.
"""
def response_list_name(self):
return "domains"
class FutureDomain(FutureResource):
def convert_callback(self, resp, body):
return Domain(self.manager, body)
def response_list_name(self):
return "domains"
class DomainsManager(base.ManagerWithFind):
"""
Manage :class:`Domain` resources.
"""
resource_class = Domain
def create(self, name):
"""Not implemented / needed yet."""
if os.environ.get("ADD_DOMAINS", "False") == 'True':
accountId = self.api.client.accountId
data = {"domains":
[
{"name": name,
"ttl":"5600",
"emailAddress":"dbaas_dns@rackspace.com",
}
]
}
resp, body = self.api.client.post("/domains", body=data)
if resp.status == 202:
return FutureDomain(self, **body)
raise RuntimeError("Did not expect response " + str(resp.status))
else:
raise NotImplementedError("No need for create.")
def create_from_list(self, list):
return [self.resource_class(self, res) for res in list]
def delete(self, *args, **kwargs):
"""Not implemented / needed yet."""
raise NotImplementedError("No need for create.")
def list(self, name=None):
"""
Get a list of all domains.
:rtype: list of :class:`Domain`
"""
url = "/domains"
if name:
url += "?name=" + name
resp, body = self.api.client.get(url)
try:
list = body['domains']
except KeyError:
raise RuntimeError('Body was missing "domains" key.')
return self.create_from_list(list)

View File

@ -0,0 +1,53 @@
# Copyright 2011 OpenStack LLC
#
# 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 novaclient import exceptions
class UnprocessableEntity(exceptions.ClientException):
"""
HTTP 422 - Unprocessable Entity: The request cannot be processed.
"""
http_status = 422
message = "Unprocessable Entity"
_code_map = dict((c.http_status, c) for c in [UnprocessableEntity])
def from_response(response, body):
"""
Return an instance of an ClientException or subclass
based on an httplib2 response.
Usage::
resp, body = http.request(...)
if resp.status != 200:
raise exception_from_response(resp, body)
"""
cls = _code_map.get(response.status, None)
if not cls:
cls = exceptions._code_map.get(response.status,
exceptions.ClientException)
if body:
message = "n/a"
details = "n/a"
if hasattr(body, 'keys'):
error = body[body.keys()[0]]
message = error.get('message', None)
details = error.get('details', None)
return cls(code=response.status, message=message, details=details)
else:
return cls(code=response.status)

54
rsdns/client/future.py Normal file
View File

@ -0,0 +1,54 @@
class RsDnsError(RuntimeError):
def __init__(self, error):
self.error_msg = ""
try:
for message in error['validationErrors']['messages']:
self.error_msg += message
except KeyError:
self.error_msg += "... (did not understand the RsDNS response)."
super(RsDnsError, self).__init__(self.error_msg)
def __str__(self):
return self.message
class FutureResource(object):
"""Polls a callback url to return a resource."""
def __init__(self, manager, jobId, callbackUrl, status, **kwargs):
self.manager = manager
self.jobId = jobId
self.callbackUrl = unicode(callbackUrl)
self.result = None
management_url = unicode(self.manager.api.client.management_url)
if self.callbackUrl.startswith(management_url):
self.callbackUrl = self.callbackUrl[len(management_url):]
def call_callback(self):
return self.manager.api.client.get(self.callbackUrl +
"?showDetails=true")
def poll(self):
if not self.result:
resp, body = self.call_callback()
if resp.status == 202:
return None
if resp.status == 200:
if body['status'] == 'ERROR':
raise RsDnsError(body['error'])
elif body['status'] != 'COMPLETED':
return None
resp_list = body['response'][self.response_list_name()]
self.result = self.manager.create_from_list(resp_list)
#self.resource_class(self, res) for res in list]
#self.result = Domain(self.manager, body['self.convert_callback(resp, body)
return self.result
@property
def ready(self):
return (self.result or self.poll()) is not None
@property
def resource(self):
return self.result or self.poll()

146
rsdns/client/records.py Normal file
View File

@ -0,0 +1,146 @@
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# 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.
"""
Records interface.
"""
import urlparse
from novaclient import base
from rsdns.client.future import FutureResource
class FutureRecord(FutureResource):
def convert_callback(self, resp, body):
try:
list = body['records']
except NameError:
raise RuntimeError('Body was missing "records" or "record" key.')
if len(list) != 1:
raise RuntimeError('Return result had ' + str(len(list)) +
'records, not 1.')
return Record(self, list[0])
def response_list_name(self):
return "records"
class Record(base.Resource):
"""
A Record is a individual dns record (Cname, A, MX, etc..)
"""
pass
class RecordsManager(base.ManagerWithFind):
"""
Manage :class:`Record` resources.
"""
resource_class = Record
def create(self, domain, record_name, record_data, record_type, record_ttl):
"""
Create a new Record on the given domain
:param domain: The ID of the :class:`Domain` to get.
:param record: The ID of the :class:`Record` to get.
:rtype: :class:`Record`
"""
data = {"records":[{"type": record_type, "name": record_name,
"data": record_data, "ttl": record_ttl }]}
resp, body = self.api.client.post("/domains/%s/records" % \
base.getid(domain), body=data)
if resp.status == 202:
return FutureRecord(self, **body)
raise RuntimeError("Did not expect response when creating a DNS record "
"%s" % str(resp.status))
def create_from_list(self, list):
return [self.resource_class(self, res) for res in list]
def delete(self, domain_id, record_id):
self._delete("/domains/%s/records/%s" % (domain_id, record_id))
def match_record(self, record, name=None, address=None, type=None):
assert(isinstance(record, Record))
return (not name or record.name == name) and \
(not address or record.data == address) and \
(not type or record.type == type)
def get(self, domain_id, record_id):
"""
Get a single record by id.
:rtype: Single instance of :class:`Record`
"""
url = "/domains/%s/records" % domain_id
if record_id:
url += ("/%s" % record_id)
resp, body = self.api.client.get(url)
try:
item = body
except IndexError:
raise RuntimeError('Body was missing record element.')
return self.resource_class(self, item)
def list(self, domain_id, record_id=None, record_name=None,
record_address=None, record_type=None):
"""
Get a list of all records under a domain.
:rtype: list of :class:`Record`
"""
url = "/domains/%s/records" % domain_id
if record_id:
url += ("/%s" % record_id)
offset = 0
list = []
while offset is not None:
next_url = "%s?offset=%d" % (url, offset)
partial_list, offset = self.page_list(next_url)
list += partial_list
all_records = self.create_from_list(list)
return [record for record in all_records
if self.match_record(record, record_name, record_address,
record_type)]
def page_list(self, url):
"""
Given a URL and an offset, returns a tuple containing a list and the
next URL.
"""
resp, body = self.api.client.get(url)
try:
list = body['records']
except NameError:
raise RuntimeError('Body was missing "records" or "record" key.')
next_offset = None
links = body.get('links', [])
for link in links:
if link['rel'] == 'next':
next = link['href']
params = urlparse.parse_qs(urlparse.urlparse(next).query)
offset_list = params.get('offset', [])
if len(offset_list) == 1:
next_offset = int(offset_list[0])
elif len(offset_list) == 0:
next_offset = None
else:
raise RuntimeError("Next href had multiple offset params!")
return (list, next_offset)