mirror of https://github.com/ansible/ansible.git
Add Amazon Elastic File System Facts (efs_facts) module
Also switch EFS module to using boto3_conn from boto3.Sessionpull/18777/head
parent
dfec3d3c4c
commit
3c6ae0063b
@ -0,0 +1,377 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# This file is part of Ansible
|
||||||
|
#
|
||||||
|
# Ansible is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# Ansible is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: efs_facts
|
||||||
|
short_description: Get information about Amazon EFS file systems
|
||||||
|
description:
|
||||||
|
- Module searches Amazon EFS file systems
|
||||||
|
version_added: "2.2"
|
||||||
|
requirements: [ boto3 ]
|
||||||
|
author:
|
||||||
|
- "Ryan Sydnor (@ryansydnor)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Creation Token of Amazon EFS file system.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
id:
|
||||||
|
description:
|
||||||
|
- ID of Amazon EFS.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
tags:
|
||||||
|
description:
|
||||||
|
- |
|
||||||
|
List of tags of Amazon EFS. Should be defined as dictionary
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
targets:
|
||||||
|
description:
|
||||||
|
- |
|
||||||
|
List of mounted targets. It should be a list of dictionaries, every dictionary should include next attributes:
|
||||||
|
- SubnetId - Mandatory. The ID of the subnet to add the mount target in.
|
||||||
|
- IpAddress - Optional. A valid IPv4 address within the address range of the specified subnet.
|
||||||
|
- SecurityGroups - Optional. List of security group IDs, of the form "sg-xxxxxxxx". These must be for the same VPC as subnet specified.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- aws
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# find all existing efs
|
||||||
|
- efs_facts:
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- efs_facts:
|
||||||
|
name: myTestNameTag
|
||||||
|
|
||||||
|
- efs_facts:
|
||||||
|
id: fs-1234abcd
|
||||||
|
|
||||||
|
# Searching all EFS instances with tag Name = 'myTestNameTag', in subnet 'subnet-1a2b3c4d' and with security group 'sg-4d3c2b1a'
|
||||||
|
- efs_facts:
|
||||||
|
tags:
|
||||||
|
name: myTestNameTag
|
||||||
|
targets:
|
||||||
|
- subnet-1a2b3c4d
|
||||||
|
- sg-4d3c2b1a
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
creation_time:
|
||||||
|
description: timestamp of creation date
|
||||||
|
returned:
|
||||||
|
type: datetime
|
||||||
|
sample: 2015-11-16 07:30:57-05:00
|
||||||
|
creation_token:
|
||||||
|
description: EFS creation token
|
||||||
|
returned:
|
||||||
|
type: UUID
|
||||||
|
sample: console-88609e04-9a0e-4a2e-912c-feaa99509961
|
||||||
|
file_system_id:
|
||||||
|
description: ID of the file system
|
||||||
|
returned:
|
||||||
|
type: unique ID
|
||||||
|
sample: fs-xxxxxxxx
|
||||||
|
life_cycle_state:
|
||||||
|
description: state of the EFS file system
|
||||||
|
returned:
|
||||||
|
type: str
|
||||||
|
sample: creating, available, deleting, deleted
|
||||||
|
mount_point:
|
||||||
|
description: url of file system
|
||||||
|
returned:
|
||||||
|
type: str
|
||||||
|
sample: .fs-xxxxxxxx.efs.us-west-2.amazonaws.com:/
|
||||||
|
mount_targets:
|
||||||
|
description: list of mount targets
|
||||||
|
returned:
|
||||||
|
type: list of dicts
|
||||||
|
sample:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"file_system_id": "fs-a7ad440e",
|
||||||
|
"ip_address": "172.31.17.173",
|
||||||
|
"life_cycle_state": "available",
|
||||||
|
"mount_target_id": "fsmt-d8907871",
|
||||||
|
"network_interface_id": "eni-6e387e26",
|
||||||
|
"owner_id": "740748460359",
|
||||||
|
"security_groups": [
|
||||||
|
"sg-a30b22c6"
|
||||||
|
],
|
||||||
|
"subnet_id": "subnet-e265c895"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
name:
|
||||||
|
description: name of the file system
|
||||||
|
returned:
|
||||||
|
type: str
|
||||||
|
sample: my-efs
|
||||||
|
number_of_mount_targets:
|
||||||
|
description: the number of targets mounted
|
||||||
|
returned:
|
||||||
|
type: int
|
||||||
|
sample: 3
|
||||||
|
owner_id:
|
||||||
|
description: AWS account ID of EFS owner
|
||||||
|
returned:
|
||||||
|
type: str
|
||||||
|
sample: XXXXXXXXXXXX
|
||||||
|
size_in_bytes:
|
||||||
|
description: size of the file system in bytes as of a timestamp
|
||||||
|
returned:
|
||||||
|
type: dict
|
||||||
|
sample:
|
||||||
|
{
|
||||||
|
"timestamp": "2015-12-21 13:59:59-05:00",
|
||||||
|
"value": 12288
|
||||||
|
}
|
||||||
|
performance_mode:
|
||||||
|
description: performance mode of the file system
|
||||||
|
returned:
|
||||||
|
type: str
|
||||||
|
sample: "generalPurpose"
|
||||||
|
tags:
|
||||||
|
description: tags on the efs instance
|
||||||
|
returned:
|
||||||
|
type: dict
|
||||||
|
sample:
|
||||||
|
{
|
||||||
|
"name": "my-efs",
|
||||||
|
"key": "Value"
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
from time import sleep
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
try:
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
import boto3
|
||||||
|
HAS_BOTO3 = True
|
||||||
|
except ImportError as e:
|
||||||
|
HAS_BOTO3 = False
|
||||||
|
|
||||||
|
class EFSConnection(object):
|
||||||
|
STATE_CREATING = 'creating'
|
||||||
|
STATE_AVAILABLE = 'available'
|
||||||
|
STATE_DELETING = 'deleting'
|
||||||
|
STATE_DELETED = 'deleted'
|
||||||
|
|
||||||
|
def __init__(self, module, region, **aws_connect_params):
|
||||||
|
try:
|
||||||
|
self.connection = boto3_conn(module, conn_type='client',
|
||||||
|
resource='efs', region=region,
|
||||||
|
**aws_connect_params)
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(msg="Failed to connect to AWS: %s" % str(e))
|
||||||
|
|
||||||
|
self.region = region
|
||||||
|
|
||||||
|
def get_file_systems(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns generator of file systems including all attributes of FS
|
||||||
|
"""
|
||||||
|
items = iterate_all(
|
||||||
|
'FileSystems',
|
||||||
|
self.connection.describe_file_systems,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
for item in items:
|
||||||
|
item['CreationTime'] = str(item['CreationTime'])
|
||||||
|
"""
|
||||||
|
Suffix of network path to be used as NFS device for mount. More detail here:
|
||||||
|
http://docs.aws.amazon.com/efs/latest/ug/gs-step-three-connect-to-ec2-instance.html
|
||||||
|
"""
|
||||||
|
item['MountPoint'] = '.%s.efs.%s.amazonaws.com:/' % (item['FileSystemId'], self.region)
|
||||||
|
if 'Timestamp' in item['SizeInBytes']:
|
||||||
|
item['SizeInBytes']['Timestamp'] = str(item['SizeInBytes']['Timestamp'])
|
||||||
|
if item['LifeCycleState'] == self.STATE_AVAILABLE:
|
||||||
|
item['Tags'] = self.get_tags(FileSystemId=item['FileSystemId'])
|
||||||
|
item['MountTargets'] = list(self.get_mount_targets(FileSystemId=item['FileSystemId']))
|
||||||
|
else:
|
||||||
|
item['Tags'] = {}
|
||||||
|
item['MountTargets'] = []
|
||||||
|
yield item
|
||||||
|
|
||||||
|
def get_tags(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns tag list for selected instance of EFS
|
||||||
|
"""
|
||||||
|
tags = iterate_all(
|
||||||
|
'Tags',
|
||||||
|
self.connection.describe_tags,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
return dict((tag['Key'], tag['Value']) for tag in tags)
|
||||||
|
|
||||||
|
def get_mount_targets(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns mount targets for selected instance of EFS
|
||||||
|
"""
|
||||||
|
targets = iterate_all(
|
||||||
|
'MountTargets',
|
||||||
|
self.connection.describe_mount_targets,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
for target in targets:
|
||||||
|
if target['LifeCycleState'] == self.STATE_AVAILABLE:
|
||||||
|
target['SecurityGroups'] = list(self.get_security_groups(
|
||||||
|
MountTargetId=target['MountTargetId']
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
target['SecurityGroups'] = []
|
||||||
|
yield target
|
||||||
|
|
||||||
|
def get_security_groups(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns security groups for selected instance of EFS
|
||||||
|
"""
|
||||||
|
return iterate_all(
|
||||||
|
'SecurityGroups',
|
||||||
|
self.connection.describe_mount_target_security_groups,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def iterate_all(attr, map_method, **kwargs):
|
||||||
|
"""
|
||||||
|
Method creates iterator from boto result set
|
||||||
|
"""
|
||||||
|
args = dict((key, value) for (key, value) in kwargs.items() if value is not None)
|
||||||
|
wait = 1
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = map_method(**args)
|
||||||
|
for elm in data[attr]:
|
||||||
|
yield elm
|
||||||
|
if 'NextMarker' in data:
|
||||||
|
args['Marker'] = data['Nextmarker']
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
except ClientError as e:
|
||||||
|
if e.response['Error']['Code'] == "ThrottlingException" and wait < 600:
|
||||||
|
sleep(wait)
|
||||||
|
wait = wait * 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
def prefix_to_attr(attr_id):
|
||||||
|
"""
|
||||||
|
Helper method to convert ID prefix to mount target attribute
|
||||||
|
"""
|
||||||
|
attr_by_prefix = {
|
||||||
|
'fsmt-': 'MountTargetId',
|
||||||
|
'subnet-': 'SubnetId',
|
||||||
|
'eni-': 'NetworkInterfaceId',
|
||||||
|
'sg-': 'SecurityGroups'
|
||||||
|
}
|
||||||
|
prefix = first_or_default(filter(
|
||||||
|
lambda pref: str(attr_id).startswith(pref),
|
||||||
|
attr_by_prefix.keys()
|
||||||
|
))
|
||||||
|
if prefix:
|
||||||
|
return attr_by_prefix[prefix]
|
||||||
|
return 'IpAddress'
|
||||||
|
|
||||||
|
def first_or_default(items, default=None):
|
||||||
|
"""
|
||||||
|
Helper method to fetch first element of list (if exists)
|
||||||
|
"""
|
||||||
|
for item in items:
|
||||||
|
return item
|
||||||
|
return default
|
||||||
|
|
||||||
|
def has_tags(available, required):
|
||||||
|
"""
|
||||||
|
Helper method to determine if tag requested already exists
|
||||||
|
"""
|
||||||
|
for key, value in required.items():
|
||||||
|
if key not in available or value != available[key]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_targets(available, required):
|
||||||
|
"""
|
||||||
|
Helper method to determine if mount tager requested already exists
|
||||||
|
"""
|
||||||
|
grouped = group_list_of_dict(available)
|
||||||
|
for (value, field) in required:
|
||||||
|
if field not in grouped or value not in grouped[field]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def group_list_of_dict(array):
|
||||||
|
"""
|
||||||
|
Helper method to group list of dict to dict with all possible values
|
||||||
|
"""
|
||||||
|
result = defaultdict(list)
|
||||||
|
for item in array:
|
||||||
|
for key, value in item.items():
|
||||||
|
result[key] += value if isinstance(value, list) else [value]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Module action handler
|
||||||
|
"""
|
||||||
|
argument_spec = ec2_argument_spec()
|
||||||
|
argument_spec.update(dict(
|
||||||
|
id=dict(required=False, type='str', default=None),
|
||||||
|
name=dict(required=False, type='str', default=None),
|
||||||
|
tags=dict(required=False, type="dict", default={}),
|
||||||
|
targets=dict(required=False, type="list", default=[])
|
||||||
|
))
|
||||||
|
|
||||||
|
module = AnsibleModule(argument_spec=argument_spec)
|
||||||
|
|
||||||
|
if not HAS_BOTO3:
|
||||||
|
module.fail_json(msg='boto3 required for this module')
|
||||||
|
|
||||||
|
region, _, aws_connect_params = get_aws_connection_info(module, boto3=True)
|
||||||
|
connection = EFSConnection(module, region, **aws_connect_params)
|
||||||
|
|
||||||
|
name = module.params.get('name')
|
||||||
|
fs_id = module.params.get('id')
|
||||||
|
tags = module.params.get('tags')
|
||||||
|
targets = module.params.get('targets')
|
||||||
|
|
||||||
|
file_systems_info = connection.get_file_systems(FileSystemId=fs_id, CreationToken=name)
|
||||||
|
|
||||||
|
if tags:
|
||||||
|
file_systems_info = filter(lambda item: has_tags(item['Tags'], tags), file_systems_info)
|
||||||
|
|
||||||
|
if targets:
|
||||||
|
targets = [(item, prefix_to_attr(item)) for item in targets]
|
||||||
|
file_systems_info = filter(lambda item:
|
||||||
|
has_targets(item['MountTargets'], targets), file_systems_info)
|
||||||
|
|
||||||
|
file_systems_info = [camel_dict_to_snake_dict(x) for x in file_systems_info]
|
||||||
|
module.exit_json(changed=False, ansible_facts={'efs': file_systems_info})
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.ec2 import *
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Loading…
Reference in New Issue