Commit 94ca263e by Edward Zarecor

Merge pull request #1548 from edx/e0d/service-builder

E0d/service builder
parents ea534476 8b7821a6
---
- name: Build application artifacts
hosts: all
connection: local
gather_facts: False
vars:
state: "present"
auto_scaling_service: True
tasks:
- name: Manage IAM Role and Profile
ec2_iam_role:
profile: "{{ profile }}"
state: "{{ state }}"
instance_profile_name: "{{ instance_profile_name }}"
role_name: "{{ role_name }}"
policies: "{{ role_policies }}"
- name: Manage ELB security group
ec2_group_local:
profile: "{{ profile }}"
description: "{{ elb_security_group.description }}"
name: "{{ elb_security_group.name }}"
vpc_id: "{{ vpc_id }}"
region: "{{ aws_region }}"
rules: "{{ elb_security_group.rules }}"
tags: "{{ elb_security_group.tags }}"
register: elb_sec_group
- debug: msg="{{ service_security_group.rules }}"
- name: Manage service security group
ec2_group_local:
profile: "{{ profile }}"
description: "{{ service_security_group.description }}"
name: "{{ service_security_group.name }}"
vpc_id: "{{ vpc_id }}"
region: "{{ aws_region }}"
rules: "{{ service_security_group.rules }}"
tags: "{{ service_security_group.tags }}"
register: service_sec_group
- name: Manage ACLs
ec2_acl:
profile: "{{ profile }}"
name: "{{ item.name }}"
vpc_id: "{{ vpc_id }}"
state: "{{ state }}"
region: "{{ aws_region }}"
rules: "{{ item.rules }}"
with_items: acls
register: created_acls
- name: Apply function to acl_data
util_map:
function: 'zip_to_dict'
input: "{{ created_acls.results }}"
args:
- "name"
- "id"
register: acl_data
# - name: Manage ELB Subnets
# ec2_subnet:
# state: "{{ state }}"
# region: "{{ aws_region }}"
# name: "{{ item.name }}"
# vpc_id: "{{ vpc_id }}"
# cidr_block: "{{ item.cidr }}"
# az: "{{ item.az }}"
# route_table_id: "{{ item.route_table_id }}"
# tags: "{{ item.tags }}"
# register: created_elb_subnets
# with_items: elb_subnets
#
# Hack alert, this registers a list in the global namespace
# of just the subnet ids that were created above
#
# - debug: msg="{{ created_elb_subnets.results|map(attribute='subnet_id')| list }}"
# register: elb_sn_list
- name: Manage Service Subnets
ec2_subnet:
profile: "{{ profile }}"
state: "{{ state }}"
region: "{{ aws_region }}"
name: "{{ item.name }}"
vpc_id: "{{ vpc_id }}"
cidr_block: "{{ item.cidr }}"
az: "{{ item.az }}"
route_table_id: "{{ item.route_table_id }}"
tags: "{{ item.tags }}"
register: created_service_subnets
with_items: service_subnets
#
# Stubbed out
# For now we'll be using an existing route table
#
# - name: Manage Route Table
# ec2_rt:
# state: "{{ state }}"
# region: "{{ aws_region }}"
# name: "{{ rt.name }}"
# vpc_id: "{{ vpc_id }}"
# destination_cidr: "{{ rt.destination_cidr }}"
# target: "local" # simplifying generalization of instnace-id, gateway-id or local
- name: Manage ELB
ec2_elb_lb:
profile: "{{ profile }}"
region: "{{ aws_region }}"
scheme: "{{ elb_scheme }}"
name: "{{ elb_name }}"
state: "{{ state }}"
security_group_ids: "{{ elb_sec_group.group_id }}"
subnets: "{{ elb_subnets }}"
health_check: "{{ elb_healthcheck }}"
listeners: "{{ elb_listeners }}"
register: elb
#
# Service related components
#
- name: Manage the launch configuration
ec2_lc:
profile: "{{ profile }}"
region: "{{ aws_region }}"
name: "{{ service_config.name }}"
image_id: "{{ service_config.ami }}"
key_name: "{{ service_config.key_name }}"
security_groups: "{{ service_sec_group.group_id }}"
instance_type: "{{ service_config.instance_type }}"
instance_profile_name: "{{ instance_profile_name }}"
volumes: "{{ service_config.volumes }}"
when: auto_scaling_service
#
# Hack alert, this registers a string in the global namespace
# of just the subnet ids for the service that were created above
#
- debug: msg="{{ created_service_subnets.results|map(attribute='subnet_id')| list | join(',') }}"
register: service_vpc_zone_identifier_string
- name: Manage ASG
ec2_asg:
profile: "{{ profile }}"
region: "{{ aws_region }}"
name: "{{ asg_name }}"
launch_config_name: "{{ service_config.name }}"
load_balancers: "{{ elb_name }}"
availability_zones: "{{ aws_availability_zones }}"
min_size: "{{ asg_min_size }}"
max_size: "{{ asg_max_size }}"
desired_capacity: "{{ asg_desired_capacity }}"
vpc_zone_identifier: "{{ service_vpc_zone_identifier_string.msg }}"
tags: "{{ asg_instance_tags }}"
register: asg
when: auto_scaling_service
- name: Manage scaling policies
ec2_scaling_policy:
state: "{{ item.state }}"
profile: "{{ item.profile }}"
region: "{{ item.region }}"
name: "{{ item.name }}"
adjustment_type: "{{ item.adjustment_type }}"
asg_name: "{{ item.asg_name }}"
scaling_adjustment: "{{ item.scaling_adjustment }}"
min_adjustment_step: "{{ item.min_adjustment_step }}"
cooldown: "{{ item.cooldown }}"
with_items: scaling_policies
register: created_policies
when: auto_scaling_service
- name: Apply function to policy data
util_map:
function: 'zip_to_dict'
input: "{{ created_policies.results }}"
args:
- "name"
- "arn"
register: policy_data
when: auto_scaling_service
- name: Manage metric alarms
ec2_metric_alarm:
profile: "{{ profile }}"
state: "{{ item.state }}"
region: "{{ aws_region }}"
name: "{{ item.name }}"
metric: "{{ item.metric }}"
namespace: "{{ item.namespace }}"
statistic: "{{ item.statistic }}"
comparison: "{{ item.comparison }}"
threshold: "{{ item.threshold }}"
period: "{{ item.period }}"
evaluation_periods: "{{ item.evaluation_periods }}"
unit: "{{ item.unit }}"
description: "{{ item.description }}"
dimensions: "{{ item.dimensions }}"
alarm_actions: "{{ policy_data.function_output[item.target_policy] }}"
with_items: metric_alarms
when: auto_scaling_service
- name: See if instances already exist
local_action:
module: "ec2_lookup"
region: "{{ aws_region }}"
tags: "{{ asg_instance_tags }}"
register: potential_existing_instances
- name: Manage instances
ec2:
profile: "{{ profile }}"
region: "{{ aws_region }}"
wait: "yes"
group_id: "{{ service_sec_group.group_id }}"
key_name: "{{ service_config.key_name }}"
vpc_subnet_id: "{{ item.subnet_id }}"
instance_type: "{{ service_config.instance_type }}"
instance_tags: "{{ asg_instance_tags }}"
image: "{{ service_config.ami }}"
instance_profile_name: "{{ instance_profile_name }}"
volumes: "{{ service_config.volumes }}"
with_items: created_service_subnets.results
when: not auto_scaling_service and potential_existing_instances.instances|length == 0
--- ---
- hosts: first_in_tag_role_mongo # Sample command: ansible-playbook -c local -i localhost, edx_vpc.yml -e@/Users/feanil/src/edx-secure/cloud_migrations/vpcs/test.yml -vvv
sudo: True - name: Create a simple empty vpc
vars_files: hosts: all
- "{{ secure_dir }}/vars/{{ENVIRONMENT}}/{{CLOUDFORMATION_STACK_NAME}}.yml" connection: local
- "{{ secure_dir }}/vars/common/common.yml" gather_facts: False
roles: vars:
- user vpc_state: present
- role: 'mongo' roles:
mongo_create_users: yes - edx_vpc
#- hosts: tag_role_mongo:!first_in_tag_role_mongo
# sudo: True
# vars_files:
# - "{{ secure_dir }}/vars/{{ENVIRONMENT}}/{{CLOUDFORMATION_STACK_NAME}}.yml"
# - "{{ secure_dir }}/vars/common/common.yml"
# roles:
# - user
# - mongo
- hosts: first_in_tag_role_edxapp
sudo: True
serial: 1
vars_files:
- "{{ secure_dir }}/vars/{{ENVIRONMENT}}/{{CLOUDFORMATION_STACK_NAME}}.yml"
- "{{ secure_dir }}/vars/common/common.yml"
roles:
- user
- datadog
- role: nginx
nginx_sites:
- lms
- cms
- lms-preview
nginx_default_sites:
- lms
- role: 'edxapp'
edxapp_lms_env: 'lms.envs.load_test'
migrate_db: '{{ RUN_EDXAPP_MIGRATION }}'
openid_workaround: 'yes'
- splunkforwarder
- hosts: tag_role_edxapp:!first_in_tag_role_edxapp
sudo: True
serial: 1
vars_files:
- "{{ secure_dir }}/vars/{{ENVIRONMENT}}/{{CLOUDFORMATION_STACK_NAME}}.yml"
- "{{ secure_dir }}/vars/common/common.yml"
roles:
- user
- datadog
- role: nginx
nginx_sites:
- lms
- cms
- lms-preview
nginx_default_site:
- lms
- role: 'edxapp'
edxapp_lms_env: 'lms.envs.load_test'
- splunkforwarder
- hosts: tag_role_worker
sudo: True
vars_files:
- "{{ secure_dir }}/vars/{{ENVIRONMENT}}/{{CLOUDFORMATION_STACK_NAME}}.yml"
- "{{ secure_dir }}/vars/common/common.yml"
roles:
- user
- datadog
- role: nginx
nginx_sites:
- lms
- cms
- lms-preview
nginx_default_site:
- lms
- role: 'edxapp'
edxapp_lms_env: 'lms.envs.load_test'
celery_worker: True
- splunkforwarder
- hosts: tag_role_xserver
sudo: True
vars_files:
- "{{ secure_dir }}/vars/{{ENVIRONMENT}}/{{CLOUDFORMATION_STACK_NAME}}.yml"
- "{{ secure_dir }}/vars/common/common.yml"
roles:
- user
- role: nginx
nginx_sites:
- xserver
- xserver
- splunkforwarder
- hosts: tag_role_rabbitmq
serial: 1
sudo: True
vars_files:
- "{{ secure_dir }}/vars/{{ENVIRONMENT}}/{{CLOUDFORMATION_STACK_NAME}}.yml"
- "{{ secure_dir }}/vars/common/common.yml"
roles:
- user
- rabbitmq
- splunkforwarder
- hosts: first_in_tag_role_xqueue
sudo: True
vars_files:
- "{{ secure_dir }}/vars/{{ENVIRONMENT}}/{{CLOUDFORMATION_STACK_NAME}}.yml"
- "{{ secure_dir }}/vars/common/common.yml"
roles:
- user
- role: nginx
nginx_sites:
- xqueue
- role: xqueue
migrate_db: '{{ RUN_XQUEUE_MIGRATION }}'
- splunkforwarder
- hosts: tag_role_xqueue:!first_in_tag_role_xqueue
sudo: True
vars_files:
- "{{ secure_dir }}/vars/{{ENVIRONMENT}}/{{CLOUDFORMATION_STACK_NAME}}.yml"
- "{{ secure_dir }}/vars/common/common.yml"
roles:
- user
- role: nginx
nginx_sites:
- xqueue
- xqueue
- splunkforwarder
- hosts: tag_role_forum
sudo: True
vars_files:
- "{{ secure_dir }}/vars/{{ENVIRONMENT}}/{{CLOUDFORMATION_STACK_NAME}}.yml"
- "{{ secure_dir }}/vars/common/common.yml"
roles:
- user
- oraclejdk
- elasticsearch
- forum
#!/usr/bin/env 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: ec2_acl
short_description: Create or delete AWS Network ACLs.
description:
- Can create or delete AwS Network ACLs.
version_added: "1.8"
author: Edward Zarecor
options:
state:
description:
- create, update or delete the acl
required: true
choices: ['present', 'absent']
name:
description:
- Unique name for acl
required: true
vpc_id:
description:
- The VPC that this acl belongs to
required: true
default: null
extends_documentation_fragment: aws
"""
EXAMPLES = '''
- ec2_acl:
name: public-acls
state: present
vpc_id: 'vpc-abababab'
'''
from ansible.module_utils.basic import *
from ansible.module_utils.ec2 import *
import sys
try:
import boto.vpc
except ImportError:
print "failed=True msg={0}".format(sys.executable)
#print "failed=True msg='boto required for this module'"
sys.exit(1)
from boto.exception import NoAuthHandlerFound
PROTOCOL_NUMBERS = {"ICMP": 1, "TCP": 6, "UDP": 17, "ALL": -1 }
class DuplicateAclError(Exception):
pass
class ACLManager():
def __init__(self, connection, vpc_id, acl_name, rules, tags=[]):
self.connection = connection
self.vpc_id = vpc_id
self.acl_name = acl_name
self.rules = rules
self.tags = tags
self.acl = None
def get_acl(self):
if not self.acl:
results = self.connection.get_all_network_acls(filters={"vpc_id": self.vpc_id, "tag:Name": self.acl_name})
if len(results) == 1:
self.acl = results[0]
elif len(results) > 1:
raise DuplicateAclError("Found multiple network acls name {0} in vpc with id {1}".
format(self.acl_name, self.vpc_id))
else:
# Does exist yet
pass
return self.acl
def create_acl(self):
self.acl = self.connection.create_network_acl(self.vpc_id)
changed = True
self.do_tags()
return changed
def update_acl(self):
changed = False
self.update_rules()
self.do_tags()
return changed
# TODO refactor out repitition
def update_rules(self):
current_ingress = [x.rule_number for x in self.acl.network_acl_entries if x.egress == 'false']
current_egress = [x.rule_number for x in self.acl.network_acl_entries if x.egress == 'true']
modified_ingress = []
modified_egress = []
for rule in self.rules:
egress = True if rule['type'] == "egress" else False
protocol = PROTOCOL_NUMBERS[rule['protocol'].upper()]
if not egress:
if rule['number'] not in current_ingress:
# new rule
self.connection.create_network_acl_entry(
self.acl.id,
rule['number'],
protocol,
rule['rule_action'],
rule['cidr_block'],
egress=egress,
port_range_from=rule['from_port'],
port_range_to=rule['to_port'])
else:
# blindly replace rather than attempting
# to determine in the entry has changed
modified_ingress.append(rule['number'])
self.connection.replace_network_acl_entry (
self.acl.id,
rule['number'],
protocol,
rule['rule_action'],
rule['cidr_block'],
egress=egress,
port_range_from=rule['from_port'],
port_range_to=rule['to_port'])
else:
if rule['number'] not in current_egress:
# new rule
self.connection.create_network_acl_entry(
self.acl.id,
rule['number'],
protocol,
rule['rule_action'],
rule['cidr_block'],
egress=egress,
port_range_from=rule['from_port'],
port_range_to=rule['to_port'])
else:
# blindly replace rather than attempting
# to determine in the entry has changed
modified_egress.append(rule['number'])
self.connection.replace_network_acl_entry (
self.acl.id,
rule['number'],
protocol,
rule['rule_action'],
rule['cidr_block'],
egress=egress,
port_range_from=rule['from_port'],
port_range_to=rule['to_port'])
removed_ingress_rule_numbers = [ c for c in current_ingress if c not in modified_ingress ]
removed_egress_rule_numbers = [ c for c in current_egress if c not in modified_egress ]
for number in removed_ingress_rule_numbers:
n = int(number)
# reserved range for AWS
if n < 32767:
self.connection.delete_network_acl_entry(self.acl.id, n, False)
for number in removed_egress_rule_numbers:
n = int(number)
# reserved range for AWS
if n < 32767:
self.connection.delete_network_acl_entry(self.acl.id, n, True)
def create_rules(self):
if self.rules is None:
return
for rule in self.rules:
egress = True if rule['type'] == "egress" else False
protocol = PROTOCOL_NUMBERS[rule['protocol'].upper()]
self.connection.create_network_acl_entry(
self.acl.id,
rule['number'],
protocol,
rule['rule_action'],
rule['cidr_block'],
egress=egress,
port_range_from=rule['from_port'],
port_range_to=rule['to_port'])
def do_tags(self):
tags = {'Name': self.acl_name}
if self.tags:
for tag in self.tags:
tags[tag['key']] = tag['value']
self.get_acl().add_tags(tags)
def present(self):
existing = self.get_acl()
if not existing:
changed = self.create_acl()
self.create_rules()
else:
changed = self.update_acl()
results = dict(changed=changed,
id=self.acl.id,
name=self.acl_name,
entries=self.rules)
return results
def absent(self):
acl = self.get_acl()
changed = False
if acl:
changed = self.connection.delete_network_acl(acl.id)
results = dict(changed=changed,
id=self.acl.id,
name=self.acl_name)
return results
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(
dict(
name=dict(required=True, type='str'),
state=dict(default='present', choices=['present', 'absent']),
vpc_id=dict(required=True, type='str'),
rules=dict(type='list'),
tags=dict(type='list'),
)
)
module = AnsibleModule(argument_spec=argument_spec)
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
profile = module.params.get('profile')
if region:
try:
connection = boto.vpc.connect_to_region(region, profile_name=profile)
except boto.exception.NoAuthHandlerFound, e:
module.fail_json(msg=str(e))
else:
module.fail_json(msg="region must be specified")
vpc_id = module.params.get('vpc_id')
acl_name = module.params.get('name')
rules_in = module.params.get('rules')
tags = module.params.get('tags')
manager = ACLManager(connection, vpc_id, acl_name, rules_in, tags)
state = module.params.get('state')
results = dict()
if state == 'present':
results = manager.present()
elif state == 'absent':
results = manager.absent()
module.exit_json(**results)
main()
#!/usr/bin/env 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: ec2_iam_role
short_description: Create or delete iam roles.
description:
- Can create or delete AwS iam roles.
version_added: "1.8"
author: Edward Zarecor
options:
state:
description:
- create, update or delete the role
required: true
choices: ['present', 'absent']
name:
description:
- Name for the role
required: true
vpc_id:
description:
- The VPC that this acl belongs to
required: true
default: null
extends_documentation_fragment: aws
"""
EXAMPLES = '''
- ec2_acl:
name: public-acls
state: present
vpc_id: 'vpc-abababab'
'''
from ansible.module_utils.basic import *
from ansible.module_utils.ec2 import *
import sys
try:
import boto
except ImportError:
print "failed=True msg='boto required for this module'"
sys.exit(1)
def present(connection, module):
profile_name = module.params.get('instance_profile_name')
role_name = module.params.get('role_name')
policies = module.params.get('policies')
fetched_profile = None
fetched_role = None
profile_arn = None
role_arn = None
try:
fetched_profile = connection.get_instance_profile(profile_name)
except boto.exception.BotoServerError as bse:
pass
if not fetched_profile:
instance_profile = connection.create_instance_profile(profile_name)
profile_arn = instance_profile.arn
else:
profile_arn = fetched_profile.arn
try:
fetched_role = connection.get_role(role_name)
except boto.exception.BotoServerError as bse:
pass
if not fetched_role:
role = connection.create_role(role_name)
role_arn = role.arn
else:
role_arn = fetched_role.arn
if not fetched_profile and not fetched_role:
connection.add_role_to_instance_profile(profile_name, role_name)
for policy in policies:
fetched_policy = None
try:
fetched_policy = connection.get_role_policy(role_name, policy['name'])
except boto.exception.BotoServerError as bse:
pass
if not fetched_policy:
connection.put_role_policy(role_name, policy['name'], policy['document'])
else:
# TODO: idempotent?
connection.put_role_policy(role_name, policy['name'], policy['document'])
module.exit_json(changed=True,
instance_profile_arn=profile_arn,
role_arn=role_arn)
def absent(connection, module):
profile_name = module.params.get('instance_profile_name')
role_name = module.params.get('role_name')
policies = module.params.get('policies')
for policy in policies:
try:
connection.delete_role_policy(role_name,policy['name'])
except boto.exception.BotoServerError as bse:
# TODO: parse code to verify that this is not found case
pass
connection.remove_role_from_instance_profile(profile_name,role_name)
connection.delete_role(role_name)
connection.delete_instance_profile(profile_name)
module.exit_json(changed=True)
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(
dict(
state=dict(default='present', choices=['present', 'absent']),
instance_profile_name=dict(required=True, type='str'),
role_name=dict(required=True, type='str'),
policies=dict(type='list')
)
)
module = AnsibleModule(argument_spec=argument_spec)
profile = module.params.get('profile')
try:
connection = boto.connect_iam(profile_name=profile)
except boto.exception.NoAuthHandlerFound, e:
module.fail_json(msg = str(e))
state = module.params.get('state')
if state == 'present':
present(connection, module)
elif state == 'absent':
absent(connection, module)
main()
...@@ -96,11 +96,15 @@ def main(): ...@@ -96,11 +96,15 @@ def main():
aws_secret_key=dict(aliases=['ec2_secret_key', 'secret_key'], aws_secret_key=dict(aliases=['ec2_secret_key', 'secret_key'],
no_log=True), no_log=True),
aws_access_key=dict(aliases=['ec2_access_key', 'access_key']), aws_access_key=dict(aliases=['ec2_access_key', 'access_key']),
tags=dict(default=None, type='dict'), tags=dict(default=None, type='list'),
) )
) )
tags = module.params.get('tags') tags = {}
for item in module.params.get('tags'):
for k,v in item.iteritems():
tags[k] = v
aws_secret_key = module.params.get('aws_secret_key') aws_secret_key = module.params.get('aws_secret_key')
aws_access_key = module.params.get('aws_access_key') aws_access_key = module.params.get('aws_access_key')
region = module.params.get('region') region = module.params.get('region')
......
#!/usr/bin/env 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: ec2_rt
short_description: Create or delete AWS Route Table
description:
- Can create or delete AwS Subnets
version_added: "1.8"
author: Edward Zarecor
options:
state:
description:
- create, update or delete the subnet
required: true
choices: ['present', 'absent']
name:
description:
- Unique name for subnet
required: true
destination_cidr:
description:
- The cidr block of the subnet
aliases: ['cidr']
vpc_id:
description:
- The VPC that this acl belongs to
required: true
default: null
extends_documentation_fragment: aws
"""
EXAMPLES = '''
'''
from ansible.module_utils.basic import *
from ansible.module_utils.ec2 import *
import sys
try:
import boto.vpc
except ImportError:
print "failed=True msg={0}".format(sys.executable)
sys.exit(1)
class DuplicateRouteTableError(Exception):
pass
class InconsistentRouteError(Exception):
pass
class RTManager():
def __init__(self, connection, vpc_id, route_name, routes, tags):
self.connection = connection
self.vpc_id = vpc_id
self.name = route_name
self.routes = routes
self.tags = tags
self.rt = None
def get_rt(self):
rt_filter = { "vpc_id": self.vpc_id,
"tag:Name": self.name,
}
results = self.connection.get_all_route_tables(filters=rt_filter)
if len(results) == 1:
self.rt = results[0]
elif len(results) > 1:
msg = "Found multiple route tables with name '{}' in vpc with id '{}'."
raise DuplicateRouteTableError(msg.format(self.name, self.vpc_id))
else:
pass
# Doesn't exist yet
return self.rt
def do_tags(self):
tags = { "Name" : self.name }
if self.tags:
for tag in self.tags:
tags[tag['key']] = tag['value']
self.rt.add_tags(tags)
def create_rt(self):
self.rt = self.connection.create_route_table(self.vpc_id)
changed = True
self.do_tags()
return changed
def routes_match(self, new_route, existing_route):
# Not the same route
if new_route['cidr'] != existing_route.destination_cidr_block:
return False
instance_matches = existing_route.instance_id \
and existing_route.instance_id == new_route['instance']
gateway_matches = existing_route.gateway_id \
and existing_route.gateway_id == new_route['gateway']
return instance_matches or gateway_matches
def update_routes(self):
changed = False
existing_routes = { x.destination_cidr_block : x for x in self.rt.routes }
for route in self.routes:
# Build the args used to call the boto API
call_args = {
"route_table_id": self.rt.id,
"destination_cidr_block": route['cidr'],
}
if "gateway" in route and "instance" in route:
msg = "Both gateway and instance specified for route" + \
"with CIDR {}"
raise InconsistentRouteError(msg.format(route['cidr']))
elif "gateway" in route:
call_args['gateway_id'] = route['gateway']
elif "instance" in route:
call_args['instance_id'] = route['instance']
else:
msg = "No gateway or instance provided for route with" + \
"CIDR {}"
raise InconsistentRouteError(msg.format(route['cidr']))
if route['cidr'] in existing_routes:
# Update the route
existing_route = existing_routes[route['cidr']]
if self.routes_match(route, existing_route):
continue
self.connection.replace_route(**call_args)
changed = True
else:
# Create a new route
self.connection.create_route(**call_args)
changed = True
return changed
def present(self):
changed = False
existing = self.get_rt()
if existing:
changed = self.update_routes()
else:
changed = self.create_rt()
self.update_routes()
results = dict(changed=changed,
id=self.rt.id,
name=self.name,
routes=self.routes,
)
return results
def absent(self):
rt = self.get_rt()
changed = False
if rt:
changed = self.connection.delet_route_table(rt.id)
results = dict(changed=changed,
id=self.rt.id,
name=self.name,
)
return results
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(
dict(
name=dict(required=True, type='str'),
state=dict(default='present', choices=['present', 'absent']),
vpc_id=dict(required=True, type='str'),
routes=dict(required=True, type='list', aliases=['dest_routes']),
tags=dict(type='list'),
)
)
module = AnsibleModule(argument_spec=argument_spec)
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
profile = module.params.get('profile')
vpc_id = module.params.get('vpc_id')
route_name = module.params.get('name')
routes = module.params.get('routes')
tags = module.params.get('tags')
if region:
try:
connection = boto.vpc.connect_to_region(region,profile_name=profile)
except boto.exception.NoAuthHandlerFound, e:
module.fail_json(msg = str(e))
else:
module.fail_json(msg="region must be specified")
manager = RTManager(connection, vpc_id, route_name, routes, tags)
state = module.params.get('state')
results = dict()
if state == 'present':
results = manager.present()
elif state == 'absent':
results = manager.absent()
module.exit_json(**results)
main()
#!/usr/bin/env 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: ec2_subnet
short_description: Create or delete AWS Subnets
description:
- Can create or delete AwS Subnets
version_added: "1.8"
author: Edward Zarecor
options:
state:
description:
- create, update or delete the subnet
required: true
choices: ['present', 'absent']
name:
description:
- Unique name for subnet
required: true
cidr_block:
description:
- The cidr block of the subnet
aliases: ['cidr']
availability_zone
description:
- The availability zone of the subnet
aliases: ['az']
vpc_id:
description:
- The VPC that this acl belongs to
required: true
default: null
extends_documentation_fragment: aws
"""
EXAMPLES = '''
'''
from ansible.module_utils.basic import *
from ansible.module_utils.ec2 import *
import sys
try:
import boto.vpc
except ImportError:
print "failed=True msg='boto required for this module'"
sys.exit(1)
from boto.exception import NoAuthHandlerFound
class NonUniqueSubnetSpecification(Exception):
pass
class SubnetManager:
def __init__(self, connection, vpc_id, cidr_block, az, name, route_table_id, network_acl_id, tags=[]):
self.connection = connection
self.vpc_id = vpc_id
self.cidr_block = cidr_block
self.az = az
self.name = name
self.route_table_id = route_table_id
self.network_acl_id = network_acl_id
self.tags = tags
self.subnet = None
def get_subnet(self):
if not self.subnet:
subnets = self.connection. \
get_all_subnets(filters={"vpc_id": self.vpc_id,
"cidr_block": self.cidr_block,
"availability_zone": self.az})
if len(subnets) > 1:
message = "Subnet specifier of vpc_id {vpc_id}, cidr_block {cidr_block} " \
"and az {az}, return more than one result".format(
vpc_id=self.vpc_id, cidr_block=self.cidr_block,az=self.az)
raise NonUniqueSubnetSpecification(message)
elif len(subnets) == 1:
self.subnet = subnets[0]
return self.subnet
def present(self):
if self.get_subnet():
changed = self.update_subnet()
else:
changed = self.create_subnet()
results = dict(changed=changed,
subnet_id=self.subnet.id,
subnet_name=self.name,
vpc_id=self.vpc_id)
return results
def create_subnet(self):
changed = True
self.subnet = self.connection.create_subnet(self.vpc_id, self.cidr_block, availability_zone=self.az)
self.do_tags()
self.connection.associate_route_table(self.route_table_id, self.subnet.id)
if self.network_acl_id:
self.connection.associate_network_acl(self.network_acl_id, self.subnet.id)
return changed
def update_subnet(self):
changed = False
self.do_tags()
results = self.connection.get_all_route_tables(
filters={'association.subnet_id': self.subnet.id, 'vpc_id': self.vpc_id})
if len(results) == 1:
route_table = results[0]
assoc = self.get_association_from_route_table(route_table, self.subnet)
if assoc.route_table_id != self.route_table_id:
self.connection.replace_route_table_association_with_assoc(assoc.id, self.route_table_id)
changed = True
elif len(results) == 0:
# unlikely unless manual monkeying around
self.connection.associate_route_table(self.route_table_id, self.subnet.id)
changed == True
if self.network_acl_id:
self.connection.associate_network_acl(self.network_acl_id, self.subnet.id)
# acl_results = self.connection.get_all_network_acls(
# filters={'association.subnet_id': self.subnet.id, 'vpc_id': self.vpc_id})
#
# if len(acl_results) == 1:
# acl = acl_results[0]
#
# if acl.id != self.network_acl_id:
# self.connection.disassociate_network_acl
return changed
def absent(self):
changed = self.connection.delete_subnet(self.subnet.id)
return dict(changed=changed)
def get_association_from_route_table(self, route_table, subnet):
target = None
for assoc in route_table.associations:
if assoc.subnet_id == subnet.id:
target = assoc
break
return target
def do_tags(self):
"""
Utility that creates all tags including the Name tag which is treated
as a first class params as a convenience. Currently updates
existing tags, as the API overwrites them, but does not remove
orphans.
:return: None
"""
tags = {'Name': self.name}
if self.tags:
for tag in self.tags:
tags[tag['key']] = tag['value']
self.subnet.add_tags(tags)
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(
dict(
name=dict(required=True, type='str'),
state=dict(default='present', choices=['present', 'absent']),
vpc_id=dict(required=True, type='str'),
cidr_block=dict(required=True, type='str', aliases=['cidr']),
az=dict(required=True, type='str'),
route_table_id=dict(required=True, type='str'),
network_acl_id=dict(type='str'),
tags=dict(type='list'),
)
)
module = AnsibleModule(argument_spec=argument_spec)
ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
profile = module.params.get('profile')
if region:
try:
connection = boto.vpc.connect_to_region(region, profile_name=profile)
except boto.exception.NoAuthHandlerFound, e:
module.fail_json(msg=str(e))
else:
module.fail_json(msg="region must be specified")
vpc_id = module.params.get('vpc_id')
cidr_block = module.params.get('cidr_block')
az = module.params.get('az')
name = module.params.get('name')
route_table_id = module.params.get('route_table_id')
network_acl_id = module.params.get('network_acl_id')
tags = module.params.get('tags')
manager = SubnetManager(connection, vpc_id, cidr_block, az, name, route_table_id, network_acl_id, tags)
state = module.params.get('state')
if state == 'present':
results = manager.present()
elif state == 'absent':
results = manager.absent()
else:
raise Exception("Unexpected value for state {0}".format(state))
module.exit_json(**results)
main()
#!/usr/bin/env python
DOCUMENTATION = """
---
module: util_map
short_description: Applies a function to input and returns the result with the key function_output
description:
- Applies functions to data structures returning a new data structure.
version_added: "1.8"
author: Edward Zarecor
options:
function:
description:
- The function to apply, currently ['zip_to_dict','flatten']
required: true
input:
description:
- The input
required: true
args
description:
- Arguments to the function other than the input, varies by function.
"""
EXAMPLES = '''
- name: Apply function to results from ec2_scaling_policies
util_map:
function: 'zip_to_dict'
input: "{{ created_policies.results }}"
args:
- "name"
- "arn"
register: policy_data
- name: Apply function to policy data
util_map:
function: 'flatten'
input:
- 'a'
- 'b'
- 'c'
- ['d','e','f']
register: flat_list
'''
from ansible.module_utils.basic import *
import ast
import itertools
class ArgumentError(Exception):
pass
def flatten(module, input):
"""
Takes an iterable and returns a flat list
With the input of
[['a','b','c'],'d',['e','f','g'],{'a','b'}]
this function will return
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'a', 'b']
:param module: The ansible module
:param input: An iterable
:return: A flat list.
"""
try:
flat = list(itertools.chain.from_iterable(input))
except TypeError as te:
raise ArgumentError("Flatten resulted in a type error, {0}.".format(te.message))
module.exit_json(function_output = flat)
def zip_to_dict(module, input, key_key, value_key):
"""
Takes an array of dicts and flattens it to a single dict by extracting the values
of a provided key as the keys in the new dict and the values of the second
provided key as the corresponding values.
For example, the input dict of
[{'name':'fred', 'id':'123'},{'name':'bill', 'id':'321'}]
with an args array of ['id','name']
would return
{'123':'fred','321':'bill'}
:param input: an array of dicts, typically the results of an ansible module
:param key_key: a key into the input dict returning a value to be used as a key in the flattened dict
:param value_key: a key into the input dict returning a value to be used as a value in the flattened dict
:return: the flattened dict
"""
results = {}
for item in input:
results[item[key_key]]=item[value_key]
module.exit_json(function_output = results)
def zip_to_list(module, input, key):
"""
Takes an array of dicts and flattens it to a single list by extracting the value
of a provided key as an item in the new list.
For example, the input list of dicts like
[{'name':'fred', 'id':'123'},{'name':'bill', 'id':'321'}]
with an args array of ['name']
would return
['fred','bill']
:param input: an array of dicts, typically the results of an ansible module
:param key: a key into the input dict returning a value to be used as an item in the flattend list
:return: the flattened list
"""
results = []
for item in input:
results.append(item[key])
module.exit_json(function_output = results)
def main():
arg_spec = dict(
function=dict(required=True, type='str'),
input=dict(required=True, type='str'),
args=dict(required=False, type='list'),
)
module = AnsibleModule(argument_spec=arg_spec, supports_check_mode=False)
target = module.params.get('function')
# reify input data
input = ast.literal_eval(module.params.get('input'))
args = module.params.get('args')
if target == 'zip_to_dict':
zip_to_dict(module, input, *args)
elif target == 'flatten':
flatten(module,input)
elif target == 'zip_to_list':
zip_to_list(module, input, *args)
else:
raise NotImplemented("Function {0} is not implemented.".format(target))
main()
---
#
# edX Configuration
#
# github: https://github.com/edx/configuration
# wiki: https://github.com/edx/configuration/wiki
# code style: https://github.com/edx/configuration/wiki/Ansible-Coding-Conventions
# license: https://github.com/edx/configuration/blob/master/LICENSE.TXT
#
##
# Defaults for role edx_vpc
#
#
# vars are namespace with the module name.
#
vpc_role_name: vpc
#
# OS packages
#
vpc_debian_pkgs: []
vpc_redhat_pkgs: []
---
#
# edX Configuration
#
# github: https://github.com/edx/configuration
# wiki: https://github.com/edx/configuration/wiki
# code style: https://github.com/edx/configuration/wiki/Ansible-Coding-Conventions
# license: https://github.com/edx/configuration/blob/master/LICENSE.TXT
#
#
#
# Tasks for role edx_vpc
#
# Overview:
# This role creates an opinionated vpc for containing cluster of edx services.
#
# It currently assumes that we will be multi-az, with a single NAT, and all
# traffic going over that NAT. A public subnet, and both public and private
# route tables are created by default that can be used by new services in this
# vpc. The public subnet should house ELBs and any newly created private subnets
# can use the existing private route table to be able to reach the internet from
# private machines.
#
#
# Example play:
#
# ansible-playbook -c local -i localhost, edx_vpc.yml -e@/Users/feanil/src/edx-secure/cloud_migrations/vpcs/test.yml
# DO NOT use the subnet or route table sections of this command.
# They will delete any subnets or rts not defined here which is
# probably not what you want, since other services were added
# to the vpc whose subnets and rts are not enumerated here.
- name: create a vpc
local_action:
module: "ec2_vpc_local"
resource_tags: "{{ vpc_tags }}"
cidr_block: "{{ vpc_cidr }}"
region: "{{ vpc_aws_region }}"
state: "{{ vpc_state }}"
internet_gateway: yes
wait: yes
register: created_vpc
# A default network acl is created when a vpc is created so each VPC
# should have one but we create one here that allows access to the
# outside world using the internet gateway.
- name: create public network acl
ec2_acl:
profile: "{{ vpc_aws_profile }}"
name: "{{ vpc_public_acl.name }}"
vpc_id: "{{ created_vpc.vpc_id }}"
state: "present"
region: "{{ vpc_aws_region }}"
rules: "{{ vpc_public_acl.rules }}"
register: created_public_acl
- name: create public route table
ec2_rt:
profile: "{{ vpc_aws_profile }}"
vpc_id: "{{ created_vpc.vpc_id }}"
region: "{{ vpc_aws_region }}"
state: "present"
name: "{{ vpc_name }}-public"
routes: "{{ vpc_public_route_table }}"
register: created_public_rt
- name: create public subnets
ec2_subnet:
profile: "{{ vpc_aws_profile }}"
vpc_id: "{{ created_vpc.vpc_id }}"
region: "{{ vpc_aws_region }}"
state: "present"
name: "{{ item.name }}"
cidr: "{{ item.cidr }}"
az: "{{ item.az }}"
route_table_id: "{{ created_public_rt.id }}"
network_acl_id: "{{ created_public_acl.id }}"
with_items: vpc_public_subnets
register: created_public_subnets
- name: create NAT security group
ec2_group:
profile: "{{ vpc_aws_profile }}"
vpc_id: "{{ created_vpc.vpc_id }}"
state: "present"
region: "{{ vpc_aws_region }}"
name: "{{ nat_security_group.name }}"
rules: "{{ nat_security_group.rules }}"
description: "{{ nat_security_group.description }}"
rules_egress: "{{ nat_security_group.rules_egress }}"
register: created_nat_security_group
- name: check to see if we already have a nat instance
local_action:
module: "ec2_lookup"
region: "{{ vpc_aws_region }}"
tags:
- Name: "{{ vpc_name }}-nat-instance"
register: nat_instance
- name: create nat instance
local_action:
module: "ec2"
state: "present"
wait: yes
source_dest_check: false
region: "{{ vpc_aws_region }}"
profile: "{{ vpc_aws_profile }}"
group_id: "{{ created_nat_security_group.group_id }}"
key_name: "{{ vpc_keypair }}"
vpc_subnet_id: "{{ created_public_subnets.results[0].subnet_id }}"
instance_type: "{{ vpc_nat_instance_type }}"
instance_tags:
Name: "{{ vpc_name }}-nat-instance"
image: "{{ vpc_nat_ami_id }}"
register: new_nat_instance
when: nat_instance.instances|length == 0
# We need to do this instead of registering the output of the above
# command because if the above command get skipped, the output does
# not contain information about the instance.
- name: lookup the created nat_instance
local_action:
module: "ec2_lookup"
region: "{{ vpc_aws_region }}"
tags:
- Name: "{{ vpc_name }}-nat-instance"
register: nat_instance
- name: assign eip to nat
ec2_eip:
profile: "{{ vpc_aws_profile }}"
region: "{{ vpc_aws_region }}"
instance_id: "{{ nat_instance.instances[0].id }}"
in_vpc: true
reuse_existing_ip_allowed: true
when: new_nat_instance.changed
- name: create private route table
ec2_rt:
profile: "{{ vpc_aws_profile }}"
vpc_id: "{{ created_vpc.vpc_id }}"
region: "{{ vpc_aws_region }}"
state: "present"
name: "{{ vpc_name }}-private"
routes: "{{ vpc_private_route_table }}"
register: created_private_rt
- name: create db network acl
ec2_acl:
profile: "{{ vpc_aws_profile }}"
name: "{{ vpc_db_acl.name }}"
vpc_id: "{{ created_vpc.vpc_id }}"
state: "present"
region: "{{ vpc_aws_region }}"
rules: "{{ vpc_db_acl.rules }}"
register: created_db_acl
- name: create db subnets
ec2_subnet:
profile: "{{ vpc_aws_profile }}"
vpc_id: "{{ created_vpc.vpc_id }}"
region: "{{ vpc_aws_region }}"
state: "present"
name: "{{ item.name }}"
cidr: "{{ item.cidr }}"
az: "{{ item.az }}"
route_table_id: "{{ created_private_rt.id }}"
network_acl_id: "{{ created_db_acl.id }}"
with_items: vpc_db_subnets
register: created_db_subnets
- name: output a vpc_config for using to build services
local_action:
module: template
src: "vpc_config.yml.j2"
dest: "~/{{ e_d }}.yml"
#
# Configuration for the environment-deployment
#
profile: "{{ vpc_aws_profile }}"
vpc_id: "{{ created_vpc.vpc_id }}"
vpc_cidr: "{{ vpc_cidr }}"
vpc_class_b: "{{ vpc_class_b }}"
env: "{{ vpc_environment }}"
deployment: "{{ vpc_deployment }}"
e_d_c: "{{ vpc_environment }}-{{ vpc_deployment }}-{{ '{{' }} cluster {{ '}}' }}"
aws_region: "{{ vpc_aws_region }}"
aws_availability_zones:
{% for subnet in vpc_public_subnets %}
- {{ subnet.az }}
{% endfor %}
# Should this be service specific
ssl_cert: "{{ vpc_ssl_cert }}"
# used for ELB
public_route_table: "{{ created_public_rt.id }}"
# used for service subnet
private_route_table: "{{ created_private_rt.id }}"
instance_key_name: "{{ vpc_keypair }}"
# subject to change #TODO: provide the correct var for the eni
nat_device: "{{ nat_instance.instances[0].id }}"
public_subnet_1: "{{ vpc_public_subnets[0].cidr }}"
public_subnet_2: "{{ vpc_public_subnets[1].cidr }}"
# /28 per AZ NEEDE?
# private_subnet_1: "{{ vpc_class_b }}.110.16/28"
# private_subnet_2: "{{ vpc_class_b }}.120.16/28"
elb_subnets:
{% for subnet in created_public_subnets.results %}
- "{{ subnet.subnet_id }}"
{% endfor %}
db_subnets:
{% for subnet in created_db_subnets %}
- "{{ subnet.subnet_id }}"
{% endfor %}
#
# Do not use vars in policies :(
# Should be specific to the service right?
role_policies: []
# - name: "{{ '{{ ' + 'e_d_c' + '}}' }}-s3-policy"
# document: |
# {
# "Statement":[
# {
# "Effect":"Allow",
# "Action":["s3:*"],
# "Resource":["arn:aws:s3:::edx-stage-edx"]
# }
# ]
# }
# - name: "{{ '{{ ' + 'e_d_c' + '}}' }}-create-instance-tags"
# document: |
# {
# "Statement": [
# {
# "Effect": "Allow",
# "Action": ["ec2:CreateTags"],
# "Resource": ["arn:aws:ec2:us-east-1:xxxxxxxxxxxx:instance/*"]
# }
# ]
# }
# - name: "{{ '{{ ' + 'e_d_c' + '}}' }}-describe-ec2"
# document: |
# {"Statement":[
# {"Resource":"*",
# "Action":["ec2:DescribeInstances","ec2:DescribeTags","ec2:DescribeVolumes"],
# "Effect":"Allow"}]}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment