2016-08-26 15:23:57 +00:00
|
|
|
#!/usr/bin/python
|
2017-08-12 16:27:11 +00:00
|
|
|
# Copyright: Ansible Project
|
|
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
|
2016-08-26 15:23:57 +00:00
|
|
|
|
2017-08-16 03:16:38 +00:00
|
|
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
2017-03-14 16:07:22 +00:00
|
|
|
'status': ['preview'],
|
2018-10-11 19:38:54 +00:00
|
|
|
'supported_by': 'community'}
|
2017-03-14 16:07:22 +00:00
|
|
|
|
2016-12-06 10:35:25 +00:00
|
|
|
|
2016-08-26 15:23:57 +00:00
|
|
|
DOCUMENTATION = '''
|
|
|
|
---
|
|
|
|
module: efs_facts
|
|
|
|
short_description: Get information about Amazon EFS file systems
|
|
|
|
description:
|
2018-10-15 18:46:24 +00:00
|
|
|
- This module can be used to search Amazon EFS file systems.
|
2016-08-26 15:23:57 +00:00
|
|
|
version_added: "2.2"
|
|
|
|
requirements: [ boto3 ]
|
|
|
|
author:
|
|
|
|
- "Ryan Sydnor (@ryansydnor)"
|
|
|
|
options:
|
|
|
|
name:
|
2018-10-15 18:46:24 +00:00
|
|
|
description:
|
|
|
|
- Creation Token of Amazon EFS file system.
|
|
|
|
aliases: [ creation_token ]
|
2016-08-26 15:23:57 +00:00
|
|
|
id:
|
2018-10-15 18:46:24 +00:00
|
|
|
description:
|
|
|
|
- ID of Amazon EFS.
|
2016-08-26 15:23:57 +00:00
|
|
|
tags:
|
2018-10-15 18:46:24 +00:00
|
|
|
description:
|
|
|
|
- List of tags of Amazon EFS. Should be defined as dictionary.
|
2016-08-26 15:23:57 +00:00
|
|
|
targets:
|
2018-10-15 18:46:24 +00:00
|
|
|
description:
|
|
|
|
- List of targets on which to filter the returned results.
|
|
|
|
- Result must match all of the specified targets, each of which can be a security group ID, a subnet ID or an IP address.
|
2016-08-26 15:23:57 +00:00
|
|
|
extends_documentation_fragment:
|
2018-01-16 12:18:51 +00:00
|
|
|
- aws
|
|
|
|
- ec2
|
2016-08-26 15:23:57 +00:00
|
|
|
'''
|
|
|
|
|
|
|
|
EXAMPLES = '''
|
2018-10-15 18:46:24 +00:00
|
|
|
- name: Find all existing efs
|
|
|
|
efs_facts:
|
2016-08-26 15:23:57 +00:00
|
|
|
register: result
|
|
|
|
|
2018-10-15 18:46:24 +00:00
|
|
|
- name: Find efs using id
|
|
|
|
efs_facts:
|
2016-08-26 15:23:57 +00:00
|
|
|
id: fs-1234abcd
|
|
|
|
|
2018-10-15 18:46:24 +00:00
|
|
|
- name: Searching all EFS instances with tag Name = 'myTestNameTag', in subnet 'subnet-1a2b3c4d' and with security group 'sg-4d3c2b1a'
|
|
|
|
efs_facts:
|
2016-08-26 15:23:57 +00:00
|
|
|
tags:
|
|
|
|
name: myTestNameTag
|
|
|
|
targets:
|
|
|
|
- subnet-1a2b3c4d
|
|
|
|
- sg-4d3c2b1a
|
|
|
|
'''
|
|
|
|
|
|
|
|
RETURN = '''
|
|
|
|
creation_time:
|
|
|
|
description: timestamp of creation date
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
|
|
|
type: str
|
|
|
|
sample: "2015-11-16 07:30:57-05:00"
|
2016-08-26 15:23:57 +00:00
|
|
|
creation_token:
|
|
|
|
description: EFS creation token
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
|
|
|
type: str
|
2016-08-26 15:23:57 +00:00
|
|
|
sample: console-88609e04-9a0e-4a2e-912c-feaa99509961
|
|
|
|
file_system_id:
|
|
|
|
description: ID of the file system
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
|
|
|
type: str
|
2016-08-26 15:23:57 +00:00
|
|
|
sample: fs-xxxxxxxx
|
|
|
|
life_cycle_state:
|
|
|
|
description: state of the EFS file system
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
2016-08-26 15:23:57 +00:00
|
|
|
type: str
|
|
|
|
sample: creating, available, deleting, deleted
|
|
|
|
mount_point:
|
2018-08-01 10:32:06 +00:00
|
|
|
description: url of file system with leading dot from the time AWS EFS required to add network suffix to EFS address
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
2016-08-26 15:23:57 +00:00
|
|
|
type: str
|
|
|
|
sample: .fs-xxxxxxxx.efs.us-west-2.amazonaws.com:/
|
2018-08-01 10:32:06 +00:00
|
|
|
filesystem_address:
|
|
|
|
description: url of file system
|
|
|
|
returned: always
|
|
|
|
type: str
|
|
|
|
sample: fs-xxxxxxxx.efs.us-west-2.amazonaws.com:/
|
2016-08-26 15:23:57 +00:00
|
|
|
mount_targets:
|
|
|
|
description: list of mount targets
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
|
|
|
type: list
|
2016-08-26 15:23:57 +00:00
|
|
|
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
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
2016-08-26 15:23:57 +00:00
|
|
|
type: str
|
|
|
|
sample: my-efs
|
|
|
|
number_of_mount_targets:
|
|
|
|
description: the number of targets mounted
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
2016-08-26 15:23:57 +00:00
|
|
|
type: int
|
|
|
|
sample: 3
|
|
|
|
owner_id:
|
|
|
|
description: AWS account ID of EFS owner
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
2016-08-26 15:23:57 +00:00
|
|
|
type: str
|
|
|
|
sample: XXXXXXXXXXXX
|
|
|
|
size_in_bytes:
|
|
|
|
description: size of the file system in bytes as of a timestamp
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
2016-08-26 15:23:57 +00:00
|
|
|
type: dict
|
|
|
|
sample:
|
|
|
|
{
|
|
|
|
"timestamp": "2015-12-21 13:59:59-05:00",
|
|
|
|
"value": 12288
|
|
|
|
}
|
|
|
|
performance_mode:
|
|
|
|
description: performance mode of the file system
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
2016-08-26 15:23:57 +00:00
|
|
|
type: str
|
|
|
|
sample: "generalPurpose"
|
2018-09-18 23:10:56 +00:00
|
|
|
throughput_mode:
|
|
|
|
description: mode of throughput for the file system
|
|
|
|
returned: when botocore >= 1.10.57
|
|
|
|
type: str
|
|
|
|
sample: "bursting"
|
|
|
|
provisioned_throughput_in_mibps:
|
|
|
|
description: throughput provisioned in Mibps
|
|
|
|
returned: when botocore >= 1.10.57 and throughput_mode is set to "provisioned"
|
|
|
|
type: float
|
|
|
|
sample: 15.0
|
2016-08-26 15:23:57 +00:00
|
|
|
tags:
|
|
|
|
description: tags on the efs instance
|
2017-04-27 11:01:11 +00:00
|
|
|
returned: always
|
2016-08-26 15:23:57 +00:00
|
|
|
type: dict
|
|
|
|
sample:
|
|
|
|
{
|
|
|
|
"name": "my-efs",
|
|
|
|
"key": "Value"
|
|
|
|
}
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
from collections import defaultdict
|
2017-07-05 13:06:08 +00:00
|
|
|
|
2016-08-26 15:23:57 +00:00
|
|
|
try:
|
2017-11-01 14:05:14 +00:00
|
|
|
import botocore
|
2017-08-12 16:27:11 +00:00
|
|
|
except ImportError:
|
2017-11-01 14:05:14 +00:00
|
|
|
pass # caught by AnsibleAWSModule
|
2017-07-05 13:06:08 +00:00
|
|
|
|
2017-11-01 14:05:14 +00:00
|
|
|
from ansible.module_utils.aws.core import AnsibleAWSModule
|
|
|
|
from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, ec2_argument_spec, AWSRetry
|
|
|
|
from ansible.module_utils.ec2 import camel_dict_to_snake_dict, boto3_tag_list_to_ansible_dict
|
2018-10-15 18:46:24 +00:00
|
|
|
from ansible.module_utils._text import to_native
|
2017-08-12 16:27:11 +00:00
|
|
|
|
2016-08-26 15:23:57 +00:00
|
|
|
|
|
|
|
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)
|
2017-11-01 14:05:14 +00:00
|
|
|
self.module = module
|
2016-08-26 15:23:57 +00:00
|
|
|
except Exception as e:
|
2018-10-15 18:46:24 +00:00
|
|
|
module.fail_json(msg="Failed to connect to AWS: %s" % to_native(e))
|
2016-08-26 15:23:57 +00:00
|
|
|
|
|
|
|
self.region = region
|
|
|
|
|
2018-02-20 15:17:31 +00:00
|
|
|
@AWSRetry.exponential_backoff(catch_extra_error_codes=['ThrottlingException'])
|
2017-11-01 14:05:14 +00:00
|
|
|
def list_file_systems(self, **kwargs):
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
2018-10-15 18:46:24 +00:00
|
|
|
Returns generator of file systems including all attributes of FS
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
2017-11-01 14:05:14 +00:00
|
|
|
paginator = self.connection.get_paginator('describe_file_systems')
|
|
|
|
return paginator.paginate(**kwargs).build_full_result()['FileSystems']
|
2016-08-26 15:23:57 +00:00
|
|
|
|
2018-02-20 15:17:31 +00:00
|
|
|
@AWSRetry.exponential_backoff(catch_extra_error_codes=['ThrottlingException'])
|
2017-11-01 14:05:14 +00:00
|
|
|
def get_tags(self, file_system_id):
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
2018-10-15 18:46:24 +00:00
|
|
|
Returns tag list for selected instance of EFS
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
2017-11-01 14:05:14 +00:00
|
|
|
paginator = self.connection.get_paginator('describe_tags')
|
|
|
|
return boto3_tag_list_to_ansible_dict(paginator.paginate(FileSystemId=file_system_id).build_full_result()['Tags'])
|
|
|
|
|
2018-02-20 15:17:31 +00:00
|
|
|
@AWSRetry.exponential_backoff(catch_extra_error_codes=['ThrottlingException'])
|
2017-11-01 14:05:14 +00:00
|
|
|
def get_mount_targets(self, file_system_id):
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
2018-10-15 18:46:24 +00:00
|
|
|
Returns mount targets for selected instance of EFS
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
2017-11-01 14:05:14 +00:00
|
|
|
paginator = self.connection.get_paginator('describe_mount_targets')
|
|
|
|
return paginator.paginate(FileSystemId=file_system_id).build_full_result()['MountTargets']
|
2016-08-26 15:23:57 +00:00
|
|
|
|
2018-02-20 15:17:31 +00:00
|
|
|
@AWSRetry.jittered_backoff(catch_extra_error_codes=['ThrottlingException'])
|
2017-11-01 14:05:14 +00:00
|
|
|
def get_security_groups(self, mount_target_id):
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
2018-10-15 18:46:24 +00:00
|
|
|
Returns security groups for selected instance of EFS
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
2017-11-01 14:05:14 +00:00
|
|
|
return self.connection.describe_mount_target_security_groups(MountTargetId=mount_target_id)['SecurityGroups']
|
|
|
|
|
2018-07-08 20:34:22 +00:00
|
|
|
def get_mount_targets_data(self, file_systems):
|
|
|
|
for item in file_systems:
|
|
|
|
if item['life_cycle_state'] == self.STATE_AVAILABLE:
|
|
|
|
try:
|
|
|
|
mount_targets = self.get_mount_targets(item['file_system_id'])
|
|
|
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
|
|
self.module.fail_json_aws(e, msg="Couldn't get EFS targets")
|
|
|
|
for mt in mount_targets:
|
|
|
|
item['mount_targets'].append(camel_dict_to_snake_dict(mt))
|
|
|
|
return file_systems
|
|
|
|
|
|
|
|
def get_security_groups_data(self, file_systems):
|
|
|
|
for item in file_systems:
|
|
|
|
if item['life_cycle_state'] == self.STATE_AVAILABLE:
|
|
|
|
for target in item['mount_targets']:
|
|
|
|
if target['life_cycle_state'] == self.STATE_AVAILABLE:
|
|
|
|
try:
|
|
|
|
target['security_groups'] = self.get_security_groups(target['mount_target_id'])
|
|
|
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
|
|
self.module.fail_json_aws(e, msg="Couldn't get EFS security groups")
|
|
|
|
else:
|
|
|
|
target['security_groups'] = []
|
|
|
|
else:
|
|
|
|
item['tags'] = {}
|
|
|
|
item['mount_targets'] = []
|
|
|
|
return file_systems
|
|
|
|
|
2017-11-01 14:05:14 +00:00
|
|
|
def get_file_systems(self, file_system_id=None, creation_token=None):
|
|
|
|
kwargs = dict()
|
|
|
|
if file_system_id:
|
|
|
|
kwargs['FileSystemId'] = file_system_id
|
|
|
|
if creation_token:
|
|
|
|
kwargs['CreationToken'] = creation_token
|
2016-08-26 15:23:57 +00:00
|
|
|
try:
|
2017-11-01 14:05:14 +00:00
|
|
|
file_systems = self.list_file_systems(**kwargs)
|
|
|
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
|
|
self.module.fail_json_aws(e, msg="Couldn't get EFS file systems")
|
|
|
|
|
|
|
|
results = list()
|
|
|
|
for item in file_systems:
|
|
|
|
item['CreationTime'] = str(item['CreationTime'])
|
|
|
|
"""
|
2018-08-01 10:32:06 +00:00
|
|
|
In the time when MountPoint was introduced there was a need to add a suffix of network path before one could use it
|
|
|
|
AWS updated it and now there is no need to add a suffix. MountPoint is left for back-compatibility purpose
|
|
|
|
And new FilesystemAddress variable is introduced for direct use with other modules (e.g. mount)
|
|
|
|
AWS documentation is available here:
|
2018-10-10 14:44:17 +00:00
|
|
|
U(https://docs.aws.amazon.com/efs/latest/ug/gs-step-three-connect-to-ec2-instance.html)
|
2017-11-01 14:05:14 +00:00
|
|
|
"""
|
|
|
|
item['MountPoint'] = '.%s.efs.%s.amazonaws.com:/' % (item['FileSystemId'], self.region)
|
2018-08-01 10:32:06 +00:00
|
|
|
item['FilesystemAddress'] = '%s.efs.%s.amazonaws.com:/' % (item['FileSystemId'], self.region)
|
|
|
|
|
2017-11-01 14:05:14 +00:00
|
|
|
if 'Timestamp' in item['SizeInBytes']:
|
|
|
|
item['SizeInBytes']['Timestamp'] = str(item['SizeInBytes']['Timestamp'])
|
|
|
|
result = camel_dict_to_snake_dict(item)
|
2018-07-08 20:34:22 +00:00
|
|
|
result['tags'] = {}
|
|
|
|
result['mount_targets'] = []
|
2017-11-01 14:05:14 +00:00
|
|
|
# Set tags *after* doing camel to snake
|
|
|
|
if result['life_cycle_state'] == self.STATE_AVAILABLE:
|
|
|
|
try:
|
|
|
|
result['tags'] = self.get_tags(result['file_system_id'])
|
|
|
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
|
|
self.module.fail_json_aws(e, msg="Couldn't get EFS tags")
|
|
|
|
results.append(result)
|
|
|
|
return results
|
2016-08-26 15:23:57 +00:00
|
|
|
|
|
|
|
|
|
|
|
def prefix_to_attr(attr_id):
|
|
|
|
"""
|
2018-10-15 18:46:24 +00:00
|
|
|
Helper method to convert ID prefix to mount target attribute
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
|
|
|
attr_by_prefix = {
|
2017-11-01 14:05:14 +00:00
|
|
|
'fsmt-': 'mount_target_id',
|
|
|
|
'subnet-': 'subnet_id',
|
|
|
|
'eni-': 'network_interface_id',
|
|
|
|
'sg-': 'security_groups'
|
2016-08-26 15:23:57 +00:00
|
|
|
}
|
2017-11-01 14:05:14 +00:00
|
|
|
return first_or_default([attr_name for (prefix, attr_name) in attr_by_prefix.items()
|
|
|
|
if str(attr_id).startswith(prefix)], 'ip_address')
|
2016-08-26 15:23:57 +00:00
|
|
|
|
2017-07-05 13:06:08 +00:00
|
|
|
|
2016-08-26 15:23:57 +00:00
|
|
|
def first_or_default(items, default=None):
|
|
|
|
"""
|
2018-10-15 18:46:24 +00:00
|
|
|
Helper method to fetch first element of list (if exists)
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
|
|
|
for item in items:
|
|
|
|
return item
|
|
|
|
return default
|
|
|
|
|
2017-07-05 13:06:08 +00:00
|
|
|
|
2016-08-26 15:23:57 +00:00
|
|
|
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
|
|
|
|
|
2017-07-05 13:06:08 +00:00
|
|
|
|
2016-08-26 15:23:57 +00:00
|
|
|
def has_targets(available, required):
|
|
|
|
"""
|
2017-11-01 14:05:14 +00:00
|
|
|
Helper method to determine if mount target requested already exists
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
2017-07-05 13:06:08 +00:00
|
|
|
|
2016-08-26 15:23:57 +00:00
|
|
|
def group_list_of_dict(array):
|
|
|
|
"""
|
2018-10-15 18:46:24 +00:00
|
|
|
Helper method to group list of dict to dict with all possible values
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
|
|
|
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():
|
|
|
|
"""
|
2018-10-15 18:46:24 +00:00
|
|
|
Module action handler
|
2016-08-26 15:23:57 +00:00
|
|
|
"""
|
|
|
|
argument_spec = ec2_argument_spec()
|
|
|
|
argument_spec.update(dict(
|
2017-07-05 13:06:08 +00:00
|
|
|
id=dict(),
|
2018-10-15 18:46:24 +00:00
|
|
|
name=dict(aliases=['creation_token']),
|
2017-07-05 13:06:08 +00:00
|
|
|
tags=dict(type="dict", default={}),
|
|
|
|
targets=dict(type="list", default=[])
|
2016-08-26 15:23:57 +00:00
|
|
|
))
|
|
|
|
|
2017-11-01 14:05:14 +00:00
|
|
|
module = AnsibleAWSModule(argument_spec=argument_spec,
|
|
|
|
supports_check_mode=True)
|
2016-08-26 15:23:57 +00:00
|
|
|
|
|
|
|
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')
|
|
|
|
|
2017-11-01 14:05:14 +00:00
|
|
|
file_systems_info = connection.get_file_systems(fs_id, name)
|
2016-08-26 15:23:57 +00:00
|
|
|
|
|
|
|
if tags:
|
2017-11-01 14:05:14 +00:00
|
|
|
file_systems_info = [item for item in file_systems_info if has_tags(item['tags'], tags)]
|
2016-08-26 15:23:57 +00:00
|
|
|
|
2018-07-08 20:34:22 +00:00
|
|
|
file_systems_info = connection.get_mount_targets_data(file_systems_info)
|
|
|
|
file_systems_info = connection.get_security_groups_data(file_systems_info)
|
|
|
|
|
2016-08-26 15:23:57 +00:00
|
|
|
if targets:
|
|
|
|
targets = [(item, prefix_to_attr(item)) for item in targets]
|
2017-11-01 14:05:14 +00:00
|
|
|
file_systems_info = [item for item in file_systems_info if has_targets(item['mount_targets'], targets)]
|
2016-08-26 15:23:57 +00:00
|
|
|
|
|
|
|
module.exit_json(changed=False, ansible_facts={'efs': file_systems_info})
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|