Merge pull request #35477 from bodhi-space/infra1931

INFRA-1931 - support (present/absent) for hosted zones in states/boto_route53
This commit is contained in:
Mike Place 2016-08-20 12:09:32 +09:00 committed by GitHub
commit ce481ae3e7
4 changed files with 456 additions and 11 deletions

View File

@ -109,6 +109,131 @@ def _get_split_zone(zone, _conn, private_zone):
return False
def describe_hosted_zones(zone_id=None, domain_name=None, region=None,
key=None, keyid=None, profile=None):
'''
Return detailed info about one, or all, zones in the bound account.
If neither zone_id nor domain_name is provided, return all zones.
Note that the return format is slightly different between the 'all'
and 'single' description types.
zone_id
The unique identifier for the Hosted Zone
domain_name
The FQDN of the Hosted Zone (including final period)
region
Region to connect to.
key
Secret key to be used.
keyid
Access key to be used.
profile
A dict with region, key and keyid, or a pillar key (string) that
contains a dict with region, key and keyid.
CLI Example:
.. code-block:: bash
salt myminion boto_route53.describe_hosted_zones domain_name=foo.bar.com. \
profile='{"region": "us-east-1", "keyd": "A12345678AB", "key": "xblahblahblah"}'
'''
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
if zone_id and domain_name:
raise SaltInvocationError('At most one of zone_id or domain_name may '
'be provided')
retries = 10
while retries:
try:
if zone_id:
zone_id = zone_id.replace('/hostedzone/',
'') if zone_id.startswith('/hostedzone/') else zone_id
ret = getattr(conn.get_hosted_zone(zone_id),
'GetHostedZoneResponse', None)
elif domain_name:
ret = getattr(conn.get_hosted_zone_by_name(domain_name),
'GetHostedZoneResponse', None)
else:
marker = None
ret = None
while marker is not '':
r = conn.get_all_hosted_zones(start_marker=marker,
zone_list=ret)
ret = r['ListHostedZonesResponse']['HostedZones']
marker = r['ListHostedZonesResponse'].get('NextMarker', '')
return ret if ret else []
except DNSServerError as e:
# if rate limit, retry:
if retries and 'Throttling' == e.code:
log.debug('Throttled by AWS API.')
time.sleep(3)
tries -= 1
continue
log.error('Could not list zones: {0}'.format(e.message))
return []
def list_all_zones_by_name(region=None, key=None, keyid=None, profile=None):
'''
List, by their FQDNs, all hosted zones in the bound account.
region
Region to connect to.
key
Secret key to be used.
keyid
Access key to be used.
profile
A dict with region, key and keyid, or a pillar key (string) that
contains a dict with region, key and keyid.
CLI Example:
.. code-block:: bash
salt myminion boto_route53.list_all_zones_by_name
'''
ret = describe_hosted_zones(region=region, key=key, keyid=keyid,
profile=profile)
return [r['Name'] for r in ret]
def list_all_zones_by_id(region=None, key=None, keyid=None, profile=None):
'''
List, by their IDs, all hosted zones in the bound account.
region
Region to connect to.
key
Secret key to be used.
keyid
Access key to be used.
profile
A dict with region, key and keyid, or a pillar key (string) that
contains a dict with region, key and keyid.
CLI Example:
.. code-block:: bash
salt myminion boto_route53.list_all_zones_by_id
'''
ret = describe_hosted_zones(region=region, key=key, keyid=keyid,
profile=profile)
return [r['Id'].replace('/hostedzone/', '') for r in ret]
def zone_exists(zone, region=None, key=None, keyid=None, profile=None,
retry_on_rate_limit=True, rate_limit_retries=5):
'''
@ -147,7 +272,7 @@ def create_zone(zone, private=False, vpc_id=None, vpc_region=None, region=None,
.. versionadded:: 2015.8.0
zone
DNZ zone to create
DNS zone to create
private
True/False if the zone will be a private zone
@ -449,3 +574,130 @@ def _wait_for_sync(status, conn, wait_for_sync):
time.sleep(20)
log.error('Timed out waiting for Route53 status update.')
return False
def create_hosted_zone(domain_name, caller_ref=None, comment='',
private_zone=False, vpc_id=None, vpc_name=None,
vpc_region=None, region=None, key=None, keyid=None,
profile=None):
'''
Create a new Route53 Hosted Zone. Returns a Python data structure with
information about the newly created Hosted Zone.
domain_name
The name of the domain. This should be a fully-specified domain, and
should terminate with a period. This is the name you have registered
with your DNS registrar. It is also the name you will delegate from your
registrar to the Amazon Route 53 delegation servers returned in response
to this request.
caller_ref
A unique string that identifies the request and that allows
create_hosted_zone() calls to be retried without the risk of executing
the operation twice. You want to provide this where possible, since
additional calls while the first is in PENDING status will be accepted
and can lead to multiple copies of the zone being created in Route53.
comment
Any comments you want to include about the hosted zone.
private_zone
Set True if creating a private hosted zone.
vpc_id
When creating a private hosted zone, either the VPC ID or VPC Name to
associate with is required. Exclusive with vpe_name. Ignored if passed
for a non-private zone.
vpc_name
When creating a private hosted zone, either the VPC ID or VPC Name to
associate with is required. Exclusive with vpe_id. Ignored if passed
for a non-private zone.
vpc_region
When creating a private hosted zone, the region of the associated VPC is
required. If not provided, an effort will be made to determine it from
vpc_id or vpc_name, if possible. If this fails, you'll need to provide
an explicit value for this option. Ignored if passed for a non-private
zone.
region
Region endpoint to connect to
key
AWS key to bind with
keyid
AWS keyid to bind with
profile
Dict, or pillar key pointing to a dict, containing AWS region/key/keyid
CLI Example::
salt myminion boto_route53.create_hosted_zone example.org
'''
if region is None:
region = 'universal'
if not domain_name.endswith('.'):
raise SaltInvocationError('Domain MUST be fully-qualified, complete '
'with ending period.')
conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile)
deets = conn.get_hosted_zone_by_name(domain_name)
if deets:
log.info('Route53 hosted zone {0} already exists'.format(domain_name))
return None
args = {'domain_name': domain_name,
'caller_ref': caller_ref,
'comment': comment,
'private_zone': private_zone}
if private_zone:
if not _exactly_one((vpc_name, vpc_id)):
raise SaltInvocationError('Either vpc_name or vpc_id is required '
'when creating a private zone.')
vpcs = __salt__['boto_vpc.describe_vpcs'](
vpc_id=vpc_id, name=vpc_name, region=region, key=key,
keyid=keyid, profile=profile).get('vpcs', [])
if vpc_region and vpcs:
vpcs = [v for v in vpcs if v['region'] == vpc_region]
if not vpcs:
log.error('Private zone requested but a VPC matching given criteria'
' not found.')
return None
if len(vpcs) > 1:
log.error('Private zone requested but multiple VPCs matching given '
'criteria found: {0}.'.format([v['id'] for v in vpcs]))
return None
vpc = vpcs[0]
if vpc_name:
vpc_id = vpc['id']
if not vpc_region:
vpc_region = vpc['region']
args.update({'vpc_id': vpc_id, 'vpc_region': vpc_region})
else:
if any((vpc_id, vpc_name, vpc_region)):
log.info('Options vpc_id, vpc_name, and vpc_region are ignored '
'when creating non-private zones.')
retries = 10
while retries:
try:
# Crazy layers of dereference...
r = conn.create_hosted_zone(**args)
r = r.CreateHostedZoneResponse.__dict__ if hasattr(r,
'CreateHostedZoneResponse') else {}
return r.get('parent', {}).get('CreateHostedZoneResponse')
except DNSServerError as e:
if retries and 'Throttling' == e.code:
log.debug('Throttled by AWS API.')
time.sleep(3)
retries -= 1
continue
log.error('Failed to create hosted zone {0}: {1}'.format(
domain_name, e.message))
return None

View File

@ -73,12 +73,9 @@ Connection module for Amazon VPC
.. versionadded:: Carbon
Functions to request, accept, delete and describe
VPC peering connections.
Named VPC peering connections can be requested using these
modules.
VPC owner accounts can accept VPC peering connections (named
or otherwise).
Functions to request, accept, delete and describe VPC peering connections.
Named VPC peering connections can be requested using these modules.
VPC owner accounts can accept VPC peering connections (named or otherwise).
Examples showing creation of VPC peering connection
@ -731,7 +728,9 @@ def describe(vpc_id=None, vpc_name=None, region=None, key=None,
keys = ('id', 'cidr_block', 'is_default', 'state', 'tags',
'dhcp_options_id', 'instance_tenancy')
return {'vpc': dict([(k, getattr(vpc, k)) for k in keys])}
_r = dict([(k, getattr(vpc, k)) for k in keys])
_r.update({'region': getattr(vpc, 'region').name})
return {'vpc': _r}
else:
return {'vpc': None}
@ -786,10 +785,12 @@ def describe_vpcs(vpc_id=None, name=None, cidr=None, tags=None,
if vpcs:
ret = []
for vpc in vpcs:
ret.append(dict((k, getattr(vpc, k)) for k in keys))
_r = dict([(k, getattr(vpc, k)) for k in keys])
_r.update({'region': getattr(vpc, 'region').name})
ret.append(_r)
return {'vpcs': ret}
else:
return {'vpcs': None}
return {'vpcs': []}
except BotoServerError as e:
return {'error': salt.utils.boto.get_error(e)}

View File

@ -73,9 +73,10 @@ passed in as a dict, or as a string to pull from pillars or minion config:
# Import Python Libs
from __future__ import absolute_import
import json
# Import Salt Libs
from salt.utils import SaltInvocationError
from salt.utils import SaltInvocationError, exactly_one
import logging
log = logging.getLogger(__name__)
@ -87,6 +88,10 @@ def __virtual__():
return 'boto_route53' if 'boto_route53.get_record' in __salt__ else False
def rr_present(*args, **kwargs):
return present(*args, **kwargs)
def present(
name,
value,
@ -267,6 +272,10 @@ def present(
return ret
def rr_absent(*args, **kwargs):
return absent(*args, **kwargs)
def absent(
name,
zone,
@ -348,3 +357,185 @@ def absent(
else:
ret['comment'] = '{0} does not exist.'.format(name)
return ret
def hosted_zone_present(name, domain_name=None, private_zone=False, comment='',
vpc_id=None, vpc_name=None, vpc_region=None,
region=None, key=None, keyid=None, profile=None):
'''
Ensure a hosted zone exists with the given attributes. Note that most
things cannot be modified once a zone is created - it must be deleted and
re-spun to update these attributes:
- private_zone (AWS API limitation).
- comment (the appropriate call exists in the AWS API and in boto3, but
has not, as of this writing, been added to boto2).
- vpc_id (same story - we really need to rewrite this module with boto3)
- vpc_name (really just a pointer to vpc_id anyway).
- vpc_region (again, supported in boto3 but not boto2).
name
The name of the state definition. This will be used as the 'caller_ref'
param if/when creating the hosted zone.
domain_name
The name of the domain. This should be a fully-specified domain, and
should terminate with a period. This is the name you have registered
with your DNS registrar. It is also the name you will delegate from your
registrar to the Amazon Route 53 delegation servers returned in response
to this request. Defaults to the value of name if not provided.
comment
Any comments you want to include about the hosted zone.
private_zone
Set True if creating a private hosted zone.
vpc_id
When creating a private hosted zone, either the VPC ID or VPC Name to
associate with is required. Exclusive with vpe_name. Ignored if passed
for a non-private zone.
vpc_name
When creating a private hosted zone, either the VPC ID or VPC Name to
associate with is required. Exclusive with vpe_id. Ignored if passed
for a non-private zone.
vpc_region
When creating a private hosted zone, the region of the associated VPC is
required. If not provided, an effort will be made to determine it from
vpc_id or vpc_name, if possible. If this fails, you'll need to provide
an explicit value for this option. Ignored if passed for a non-private
zone.
'''
domain_name = domain_name if domain_name else name
ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
# First translaste vpc_name into a vpc_id if possible
if private_zone:
if not exactly_one((vpc_name, vpc_id)):
raise SaltInvocationError('Either vpc_name or vpc_id is required '
'when creating a private zone.')
vpcs = __salt__['boto_vpc.describe_vpcs'](
vpc_id=vpc_id, name=vpc_name, region=region, key=key,
keyid=keyid, profile=profile).get('vpcs', [])
if vpc_region and vpcs:
vpcs = [v for v in vpcs if v['region'] == vpc_region]
if not vpcs:
msg = ('Private zone requested but a VPC matching given criteria '
'not found.')
log.error(msg)
ret['result'] = False
ret['comment'] = msg
return ret
if len(vpcs) > 1:
msg = ('Private zone requested but multiple VPCs matching given '
'criteria found: {0}.'.format([v['id'] for v in vpcs]))
log.error(msg)
return None
vpc = vpcs[0]
if vpc_name:
vpc_id = vpc['id']
if not vpc_region:
vpc_region = vpc['region']
# Next, see if it (or they) exist at all, anywhere?
deets = __salt__['boto_route53.describe_hosted_zones'](
domain_name=domain_name, region=region, key=key, keyid=keyid,
profile=profile)
create = False
if not deets:
create = True
else: # Something exists - now does it match our criteria?
if (json.loads(deets['HostedZone']['Config']['PrivateZone']) !=
private_zone):
create = True
else:
if private_zone:
for v, d in deets.get('VPCs', {}).items():
if (d['VPCId'] == vpc_id
and d['VPCRegion'] == vpc_region):
create = False
break
else:
create = True
if not create:
ret['comment'] = 'Hostd Zone {0} already in desired state'.format(
domain_name)
else:
# Until we get modifies in place with boto3, the best option is to
# attempt creation and let route53 tell us if we're stepping on
# toes. We can't just fail, because some scenarios (think split
# horizon DNS) require zones with identical names but different
# settings...
log.info('A Hosted Zone with name {0} already exists, but with '
'different settings. Will attempt to create the one '
'requested on the assumption this is what is desired. '
'This may fail...'.format(domain_name))
if create:
if __opts__['test']:
ret['comment'] = 'Route53 Hosted Zone {0} set to be added.'.format(
domain_name)
ret['result'] = None
return ret
res = __salt__['boto_route53.create_hosted_zone'](domain_name=domain_name,
caller_ref=name, comment=comment, private_zone=private_zone,
vpc_id=vpc_id, vpc_region=vpc_region, region=region, key=key,
keyid=keyid, profile=profile)
if res:
msg = 'Hosted Zone {0} successfully created'.format(domain_name)
log.info(msg)
ret['comment'] = msg
ret['changes']['old'] = None
ret['changes']['new'] = res
else:
ret['comment'] = 'Creating Hosted Zone {0} failed'.format(
domain_name)
ret['result'] = False
return ret
def hosted_zone_absent(name, domain_name=None, region=None, key=None,
keyid=None, profile=None):
'''
Ensure the Route53 Hostes Zone described is absent
name
The name of the state definition.
domain_name
The FQDN (including final period) of the zone you wish absent. If not
provided, the value of name will be used.
'''
domain_name = domain_name if domain_name else name
ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
deets = __salt__['boto_route53.describe_hosted_zones'](
domain_name=domain_name, region=region, key=key, keyid=keyid,
profile=profile)
if not deets:
ret['comment'] = 'Hosted Zone {0} already absent'.format(domain_name)
log.info(ret['comment'])
return ret
if __opts__['test']:
ret['comment'] = 'Route53 Hosted Zone {0} set to be deleted.'.format(
domain_name)
ret['result'] = None
return ret
# Not entirely comfortable with this - no safety checks around pub/priv, VPCs
# or anything else. But this is all the module function exposes, so hmph.
# Inclined to put it on the "wait 'til we port to boto3" pile in any case :)
if __salt__['boto_route53.delete_zone'](
zone=domain_name, region=region, key=key, keyid=keyid,
profile=profile):
ret['comment'] = 'Route53 Hosted Zone {0} deleted'.format(domain_name)
log.info(ret['comment'])
ret['changes']['old'] = deets
ret['changes']['new'] = None
return ret

View File

@ -533,6 +533,7 @@ class BotoVpcTestCase(BotoVpcTestCaseBase, BotoVpcTestCaseMixin):
state=u'available',
tags={u'Name': u'test', u'test': u'testvalue'},
dhcp_options_id=u'dopt-7a8b9c2d',
region=u'us-east-1',
instance_tenancy=u'default')
self.assertEqual(describe_vpc, {'vpc': vpc_properties})