mirror of
https://github.com/valitydev/salt.git
synced 2024-11-08 01:18:58 +00:00
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:
commit
ce481ae3e7
@ -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
|
||||
|
@ -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)}
|
||||
|
@ -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
|
||||
|
@ -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})
|
||||
|
Loading…
Reference in New Issue
Block a user