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:
parent
758dce6dd8
commit
0c34c75dc4
@ -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
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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)),
|
||||
|
@ -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])
|
@ -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
18
reddwarf/dns/__init__.py
Normal 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
117
reddwarf/dns/driver.py
Normal 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
85
reddwarf/dns/manager.py
Normal 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()
|
15
reddwarf/dns/rsdns/__init__.py
Normal file
15
reddwarf/dns/rsdns/__init__.py
Normal 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.
|
221
reddwarf/dns/rsdns/driver.py
Normal file
221
reddwarf/dns/rsdns/driver.py
Normal 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)
|
||||
|
||||
|
||||
|
80
reddwarf/dns/rsdns/models.py
Normal file
80
reddwarf/dns/rsdns/models.py
Normal 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
|
@ -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):
|
||||
|
@ -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
22
rsdns/__init__.py
Normal 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
24
rsdns/client/__init__.py
Normal 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
122
rsdns/client/dns_client.py
Normal 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
89
rsdns/client/domains.py
Normal 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)
|
53
rsdns/client/exceptions.py
Normal file
53
rsdns/client/exceptions.py
Normal 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
54
rsdns/client/future.py
Normal 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
146
rsdns/client/records.py
Normal 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)
|
||||
|
Loading…
Reference in New Issue
Block a user