diff --git a/reference-architecture/aws-ansible/add-node.py b/reference-architecture/aws-ansible/add-node.py new file mode 100755 index 000000000..4f3968a4f --- /dev/null +++ b/reference-architecture/aws-ansible/add-node.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python +# vim: sw=2 ts=2 + +import click +import os +import sys + +@click.command() + +### Cluster options +@click.option('--console-port', default='443', type=click.IntRange(1,65535), help='OpenShift web console port', + show_default=True) +@click.option('--deployment-type', default='openshift-enterprise', help='OpenShift deployment type', + show_default=True) + +### AWS/EC2 options +@click.option('--region', default='us-east-1', help='ec2 region', + show_default=True) +@click.option('--ami', default='ami-10251c7a', help='ec2 ami', + show_default=True) +@click.option('--node-instance-type', default='t2.medium', help='ec2 instance type', + show_default=True) +@click.option('--keypair', help='ec2 keypair name', + show_default=True) +@click.option('--subnet-id', help='Specify a Private subnet within the existing VPC', + show_default=True) + +### DNS options +@click.option('--public-hosted-zone', help='hosted zone for accessing the environment') +@click.option('--app-dns-prefix', default='apps', help='application dns prefix', + show_default=True) + +### Subscription and Software options +@click.option('--rhsm-user', help='Red Hat Subscription Management User') +@click.option('--rhsm-password', help='Red Hat Subscription Management Password', + hide_input=True,) +@click.option('--rhsm-pool', help='Red Hat Subscription Management Pool ID or Subscription Name') + +### Miscellaneous options +@click.option('--iam-role', help='Specify the name of the existing IAM Instance profile', + show_default=True) +@click.option('--shortname', help='Specify the hostname of the sytem', + show_default=True) +@click.option('--node-sg', help='Specify the already existing node security group id', + show_default=True) +@click.option('--infra-sg', help='Specify the already existing Infrastructure node security group id', + show_default=True) +@click.option('--node-type', default='app', help='Specify the node label (example: infra or app)', + show_default=True) +@click.option('--infra-elb-name', help='Specify the name of the ELB used for the router and registry') +@click.option('--no-confirm', is_flag=True, + help='Skip confirmation prompt') +@click.help_option('--help', '-h') +@click.option('-v', '--verbose', count=True) + +def launch_refarch_env(region=None, + ami=None, + no_confirm=False, + node_instance_type=None, + keypair=None, + subnet_id=None, + node_sg=None, + infra_sg=None, + public_hosted_zone=None, + app_dns_prefix=None, + shortname=None, + fqdn=None, + deployment_type=None, + console_port=443, + rhsm_user=None, + rhsm_password=None, + rhsm_pool=None, + node_type=None, + iam_role=None, + infra_elb_name=None, + verbose=0): + + # Need to prompt for the R53 zone: + if public_hosted_zone is None: + public_hosted_zone = click.prompt('Hosted DNS zone for accessing the environment') + + if iam_role is None: + iam_role = click.prompt('Specify the name of the existing IAM Instance Profile') + + if node_sg is None: + node_sg = click.prompt('Node Security group') + + if node_type in 'infra' and infra_sg is None: + infra_sg = click.prompt('Infra Node Security group') + + if shortname is None: + shortname = click.prompt('Hostname of newly created system') + + # If no keypair is not specified fail: + if keypair is None and create_key in 'no': + click.echo('A SSH keypair must be specified or created') + sys.exit(1) + + # Name the keypair if a path is defined + if keypair is None and create_key in 'yes': + keypair = click.prompt('Specify a name for the keypair') + + # If no subnets are defined prompt: + if subnet_id is None: + subnet_id = click.prompt('Specify a Private subnet within the existing VPC') + + # If the user already provided values, don't bother asking again + if rhsm_user is None: + rhsm_user = click.prompt("RHSM username?") + if rhsm_password is None: + rhsm_password = click.prompt("RHSM password?") + if rhsm_pool is None: + rhsm_pool = click.prompt("RHSM Pool ID or Subscription Name?") + + # Calculate various DNS values + wildcard_zone="%s.%s" % (app_dns_prefix, public_hosted_zone) + + # Calculate various DNS values + fqdn="%s.%s" % (shortname, public_hosted_zone) + + # Ask for ELB if new node is infra + if node_type in 'infra' and infra_elb_name is None: + infra_elb_name = click.prompt("Specify the ELB Name used by the router and registry?") + + # Display information to the user about their choices + click.echo('Configured values:') + click.echo('\tami: %s' % ami) + click.echo('\tregion: %s' % region) + click.echo('\tnode_instance_type: %s' % node_instance_type) + click.echo('\tkeypair: %s' % keypair) + click.echo('\tsubnet_id: %s' % subnet_id) + click.echo('\tnode_sg: %s' % node_sg) + click.echo('\tinfra_sg: %s' % infra_sg) + click.echo('\tconsole port: %s' % console_port) + click.echo('\tdeployment_type: %s' % deployment_type) + click.echo('\tpublic_hosted_zone: %s' % public_hosted_zone) + click.echo('\tapp_dns_prefix: %s' % app_dns_prefix) + click.echo('\tapps_dns: %s' % wildcard_zone) + click.echo('\tshortname: %s' % shortname) + click.echo('\tfqdn: %s' % fqdn) + click.echo('\trhsm_user: %s' % rhsm_user) + click.echo('\trhsm_password: *******') + click.echo('\trhsm_pool: %s' % rhsm_pool) + click.echo('\tnode_type: %s' % node_type) + click.echo('\tiam_role: %s' % iam_role) + click.echo('\tinfra_elb_name: %s' % infra_elb_name) + click.echo("") + + if not no_confirm: + click.confirm('Continue using these values?', abort=True) + + playbooks = ['playbooks/infrastructure.yaml', 'playbooks/add-node.yaml'] + + for playbook in playbooks: + + # hide cache output unless in verbose mode + devnull='> /dev/null' + + if verbose > 0: + devnull='' + + # refresh the inventory cache to prevent stale hosts from + # interferring with re-running + command='inventory/aws/hosts/ec2.py --refresh-cache %s' % (devnull) + os.system(command) + + # remove any cached facts to prevent stale data during a re-run + command='rm -rf .ansible/cached_facts' + os.system(command) + + command='ansible-playbook -i inventory/aws/hosts -e \'region=%s \ + ami=%s \ + keypair=%s \ + add_node=yes \ + create_key=no \ + create_vpc=no \ + subnet_id=%s \ + node_sg=%s \ + infra_sg=%s \ + node_instance_type=%s \ + public_hosted_zone=%s \ + wildcard_zone=%s \ + shortname=%s \ + fqdn=%s \ + console_port=%s \ + deployment_type=%s \ + rhsm_user=%s \ + rhsm_password=%s \ + rhsm_pool=%s \ + node_type=%s \ + iam_role=%s \ + infra_elb_name=%s \' %s' % (region, + ami, + keypair, + subnet_id, + node_sg, + infra_sg, + node_instance_type, + public_hosted_zone, + wildcard_zone, + shortname, + fqdn, + console_port, + deployment_type, + rhsm_user, + rhsm_password, + rhsm_pool, + node_type, + iam_role, + infra_elb_name, + playbook) + + if verbose > 0: + command += " -" + "".join(['v']*verbose) + click.echo('We are running: %s' % command) + + status = os.system(command) + if os.WIFEXITED(status) and os.WEXITSTATUS(status) != 0: + return os.WEXITSTATUS(status) + +if __name__ == '__main__': + # check for AWS access info + if os.getenv('AWS_ACCESS_KEY_ID') is None or os.getenv('AWS_SECRET_ACCESS_KEY') is None: + print 'AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY **MUST** be exported as environment variables.' + sys.exit(1) + + launch_refarch_env(auto_envvar_prefix='OSE_REFArch') diff --git a/reference-architecture/aws-ansible/inventory/aws/hosts/ec2.py b/reference-architecture/aws-ansible/inventory/aws/hosts/ec2.py index a94adabd8..3def6037a 100755 --- a/reference-architecture/aws-ansible/inventory/aws/hosts/ec2.py +++ b/reference-architecture/aws-ansible/inventory/aws/hosts/ec2.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python ''' EC2 external inventory script @@ -22,6 +22,12 @@ export EC2_URL=http://hostname_of_your_cc:port/services/Eucalyptus +If you're using boto profiles (requires boto>=2.24.0) you can choose a profile +using the --boto-profile command line argument (e.g. ec2.py --boto-profile prod) or using +the AWS_PROFILE variable: + + AWS_PROFILE=prod ansible-playbook -i ec2.py myplaybook.yml + For more details, see: http://docs.pythonboto.org/en/latest/boto_config_tut.html When run against a specific host, this script returns the following variables: @@ -31,6 +37,7 @@ - ec2_attachTime - ec2_attachment - ec2_attachmentId + - ec2_block_devices - ec2_client_token - ec2_deleteOnTermination - ec2_description @@ -121,8 +128,20 @@ import boto from boto import ec2 from boto import rds +from boto import elasticache from boto import route53 -import ConfigParser +import six + +from ansible.module_utils import ec2 as ec2_utils + +HAS_BOTO3 = False +try: + import boto3 + HAS_BOTO3 = True +except ImportError: + pass + +from six.moves import configparser from collections import defaultdict try: @@ -132,6 +151,7 @@ class Ec2Inventory(object): + def _empty_inventory(self): return {"_meta" : {"hostvars" : {}}} @@ -145,9 +165,21 @@ def __init__(self): # Index of hostname (address) to instance ID self.index = {} + # Boto profile to use (if any) + self.boto_profile = None + + # AWS credentials. + self.credentials = {} + # Read settings and parse CLI arguments - self.read_settings() self.parse_cli_args() + self.read_settings() + + # Make sure that profile_name is not passed at all if not set + # as pre 2.24 boto will fall over otherwise + if self.boto_profile: + if not hasattr(boto.ec2.EC2Connection, 'profile_name'): + self.fail_with_error("boto version must be >= 2.24 to use profile") # Cache if self.args.refresh_cache: @@ -166,7 +198,7 @@ def __init__(self): else: data_to_print = self.json_format_dict(self.inventory, True) - print data_to_print + print(data_to_print) def is_cache_valid(self): @@ -184,10 +216,12 @@ def is_cache_valid(self): def read_settings(self): ''' Reads the settings from the ec2.ini file ''' - - config = ConfigParser.SafeConfigParser() + if six.PY3: + config = configparser.ConfigParser() + else: + config = configparser.SafeConfigParser() ec2_default_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ec2.ini') - ec2_ini_path = os.environ.get('EC2_INI_PATH', ec2_default_ini_path) + ec2_ini_path = os.path.expanduser(os.path.expandvars(os.environ.get('EC2_INI_PATH', ec2_default_ini_path))) config.read(ec2_ini_path) # is eucalyptus? @@ -204,7 +238,7 @@ def read_settings(self): configRegions_exclude = config.get('ec2', 'regions_exclude') if (configRegions == 'all'): if self.eucalyptus_host: - self.regions.append(boto.connect_euca(host=self.eucalyptus_host).region.name) + self.regions.append(boto.connect_euca(host=self.eucalyptus_host).region.name, **self.credentials) else: for regionInfo in ec2.regions(): if regionInfo.name not in configRegions_exclude: @@ -216,6 +250,11 @@ def read_settings(self): self.destination_variable = config.get('ec2', 'destination_variable') self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable') + if config.has_option('ec2', 'hostname_variable'): + self.hostname_variable = config.get('ec2', 'hostname_variable') + else: + self.hostname_variable = None + if config.has_option('ec2', 'destination_format') and \ config.has_option('ec2', 'destination_format_tags'): self.destination_format = config.get('ec2', 'destination_format') @@ -236,31 +275,132 @@ def read_settings(self): if config.has_option('ec2', 'rds'): self.rds_enabled = config.getboolean('ec2', 'rds') - # Return all EC2 and RDS instances (if RDS is enabled) + # Include RDS cluster instances? + if config.has_option('ec2', 'include_rds_clusters'): + self.include_rds_clusters = config.getboolean('ec2', 'include_rds_clusters') + else: + self.include_rds_clusters = False + + # Include ElastiCache instances? + self.elasticache_enabled = True + if config.has_option('ec2', 'elasticache'): + self.elasticache_enabled = config.getboolean('ec2', 'elasticache') + + # Return all EC2 instances? if config.has_option('ec2', 'all_instances'): self.all_instances = config.getboolean('ec2', 'all_instances') else: self.all_instances = False + + # Instance states to be gathered in inventory. Default is 'running'. + # Setting 'all_instances' to 'yes' overrides this option. + ec2_valid_instance_states = [ + 'pending', + 'running', + 'shutting-down', + 'terminated', + 'stopping', + 'stopped' + ] + self.ec2_instance_states = [] + if self.all_instances: + self.ec2_instance_states = ec2_valid_instance_states + elif config.has_option('ec2', 'instance_states'): + for instance_state in config.get('ec2', 'instance_states').split(','): + instance_state = instance_state.strip() + if instance_state not in ec2_valid_instance_states: + continue + self.ec2_instance_states.append(instance_state) + else: + self.ec2_instance_states = ['running'] + + # Return all RDS instances? (if RDS is enabled) if config.has_option('ec2', 'all_rds_instances') and self.rds_enabled: self.all_rds_instances = config.getboolean('ec2', 'all_rds_instances') else: self.all_rds_instances = False + # Return all ElastiCache replication groups? (if ElastiCache is enabled) + if config.has_option('ec2', 'all_elasticache_replication_groups') and self.elasticache_enabled: + self.all_elasticache_replication_groups = config.getboolean('ec2', 'all_elasticache_replication_groups') + else: + self.all_elasticache_replication_groups = False + + # Return all ElastiCache clusters? (if ElastiCache is enabled) + if config.has_option('ec2', 'all_elasticache_clusters') and self.elasticache_enabled: + self.all_elasticache_clusters = config.getboolean('ec2', 'all_elasticache_clusters') + else: + self.all_elasticache_clusters = False + + # Return all ElastiCache nodes? (if ElastiCache is enabled) + if config.has_option('ec2', 'all_elasticache_nodes') and self.elasticache_enabled: + self.all_elasticache_nodes = config.getboolean('ec2', 'all_elasticache_nodes') + else: + self.all_elasticache_nodes = False + + # boto configuration profile (prefer CLI argument) + self.boto_profile = self.args.boto_profile + if config.has_option('ec2', 'boto_profile') and not self.boto_profile: + self.boto_profile = config.get('ec2', 'boto_profile') + + # AWS credentials (prefer environment variables) + if not (self.boto_profile or os.environ.get('AWS_ACCESS_KEY_ID') or + os.environ.get('AWS_PROFILE')): + if config.has_option('credentials', 'aws_access_key_id'): + aws_access_key_id = config.get('credentials', 'aws_access_key_id') + else: + aws_access_key_id = None + if config.has_option('credentials', 'aws_secret_access_key'): + aws_secret_access_key = config.get('credentials', 'aws_secret_access_key') + else: + aws_secret_access_key = None + if config.has_option('credentials', 'aws_security_token'): + aws_security_token = config.get('credentials', 'aws_security_token') + else: + aws_security_token = None + if aws_access_key_id: + self.credentials = { + 'aws_access_key_id': aws_access_key_id, + 'aws_secret_access_key': aws_secret_access_key + } + if aws_security_token: + self.credentials['security_token'] = aws_security_token + # Cache related cache_dir = os.path.expanduser(config.get('ec2', 'cache_path')) + if self.boto_profile: + cache_dir = os.path.join(cache_dir, 'profile_' + self.boto_profile) if not os.path.exists(cache_dir): os.makedirs(cache_dir) - self.cache_path_cache = cache_dir + "/ansible-ec2.cache" - self.cache_path_index = cache_dir + "/ansible-ec2.index" + cache_name = 'ansible-ec2' + aws_profile = lambda: (self.boto_profile or + os.environ.get('AWS_PROFILE') or + os.environ.get('AWS_ACCESS_KEY_ID') or + self.credentials.get('aws_access_key_id', None)) + if aws_profile(): + cache_name = '%s-%s' % (cache_name, aws_profile()) + self.cache_path_cache = cache_dir + "/%s.cache" % cache_name + self.cache_path_index = cache_dir + "/%s.index" % cache_name self.cache_max_age = config.getint('ec2', 'cache_max_age') + if config.has_option('ec2', 'expand_csv_tags'): + self.expand_csv_tags = config.getboolean('ec2', 'expand_csv_tags') + else: + self.expand_csv_tags = False + # Configure nested groups instead of flat namespace. if config.has_option('ec2', 'nested_groups'): self.nested_groups = config.getboolean('ec2', 'nested_groups') else: self.nested_groups = False + # Replace dash or not in group names + if config.has_option('ec2', 'replace_dash_in_groups'): + self.replace_dash_in_groups = config.getboolean('ec2', 'replace_dash_in_groups') + else: + self.replace_dash_in_groups = True + # Configure which groups should be created. group_by_options = [ 'group_by_instance_id', @@ -276,6 +416,10 @@ def read_settings(self): 'group_by_route53_names', 'group_by_rds_engine', 'group_by_rds_parameter_group', + 'group_by_elasticache_engine', + 'group_by_elasticache_cluster', + 'group_by_elasticache_parameter_group', + 'group_by_elasticache_replication_group', ] for option in group_by_options: if config.has_option('ec2', option): @@ -290,7 +434,7 @@ def read_settings(self): self.pattern_include = re.compile(pattern_include) else: self.pattern_include = None - except ConfigParser.NoOptionError, e: + except configparser.NoOptionError: self.pattern_include = None # Do we need to exclude hosts that match a pattern? @@ -300,13 +444,16 @@ def read_settings(self): self.pattern_exclude = re.compile(pattern_exclude) else: self.pattern_exclude = None - except ConfigParser.NoOptionError, e: + except configparser.NoOptionError: self.pattern_exclude = None # Instance filters (see boto and EC2 API docs). Ignore invalid filters. self.ec2_instance_filters = defaultdict(list) if config.has_option('ec2', 'instance_filters'): - for instance_filter in config.get('ec2', 'instance_filters', '').split(','): + + filters = [f for f in config.get('ec2', 'instance_filters').split(',') if f] + + for instance_filter in filters: instance_filter = instance_filter.strip() if not instance_filter or '=' not in instance_filter: continue @@ -325,6 +472,8 @@ def parse_cli_args(self): help='Get all the variables about a specific instance') parser.add_argument('--refresh-cache', action='store_true', default=False, help='Force refresh of cache by making API requests to EC2 (default: False - use cache files)') + parser.add_argument('--profile', '--boto-profile', action='store', dest='boto_profile', + help='Use boto profile for connections to EC2') self.args = parser.parse_args() @@ -338,72 +487,267 @@ def do_api_calls_update_cache(self): self.get_instances_by_region(region) if self.rds_enabled: self.get_rds_instances_by_region(region) + if self.elasticache_enabled: + self.get_elasticache_clusters_by_region(region) + self.get_elasticache_replication_groups_by_region(region) + if self.include_rds_clusters: + self.include_rds_clusters_by_region(region) self.write_to_cache(self.inventory, self.cache_path_cache) self.write_to_cache(self.index, self.cache_path_index) + def connect(self, region): + ''' create connection to api server''' + if self.eucalyptus: + conn = boto.connect_euca(host=self.eucalyptus_host, **self.credentials) + conn.APIVersion = '2010-08-31' + else: + conn = self.connect_to_aws(ec2, region) + return conn + + def boto_fix_security_token_in_profile(self, connect_args): + ''' monkey patch for boto issue boto/boto#2100 ''' + profile = 'profile ' + self.boto_profile + if boto.config.has_option(profile, 'aws_security_token'): + connect_args['security_token'] = boto.config.get(profile, 'aws_security_token') + return connect_args + + def connect_to_aws(self, module, region): + connect_args = self.credentials + + # only pass the profile name if it's set (as it is not supported by older boto versions) + if self.boto_profile: + connect_args['profile_name'] = self.boto_profile + self.boto_fix_security_token_in_profile(connect_args) + + conn = module.connect_to_region(region, **connect_args) + # connect_to_region will fail "silently" by returning None if the region name is wrong or not supported + if conn is None: + self.fail_with_error("region name: %s likely not supported, or AWS is down. connection to region failed." % region) + return conn def get_instances_by_region(self, region): ''' Makes an AWS EC2 API call to the list of instances in a particular region ''' try: - if self.eucalyptus: - conn = boto.connect_euca(host=self.eucalyptus_host) - conn.APIVersion = '2010-08-31' - else: - conn = ec2.connect_to_region(region) - - # connect_to_region will fail "silently" by returning None if the region name is wrong or not supported - if conn is None: - print("region name: %s likely not supported, or AWS is down. connection to region failed." % region) - sys.exit(1) - + conn = self.connect(region) reservations = [] if self.ec2_instance_filters: - for filter_key, filter_values in self.ec2_instance_filters.iteritems(): + for filter_key, filter_values in self.ec2_instance_filters.items(): reservations.extend(conn.get_all_instances(filters = { filter_key : filter_values })) else: reservations = conn.get_all_instances() + # Pull the tags back in a second step + # AWS are on record as saying that the tags fetched in the first `get_all_instances` request are not + # reliable and may be missing, and the only way to guarantee they are there is by calling `get_all_tags` + instance_ids = [] + for reservation in reservations: + instance_ids.extend([instance.id for instance in reservation.instances]) + + max_filter_value = 199 + tags = [] + for i in range(0, len(instance_ids), max_filter_value): + tags.extend(conn.get_all_tags(filters={'resource-type': 'instance', 'resource-id': instance_ids[i:i+max_filter_value]})) + + tags_by_instance_id = defaultdict(dict) + for tag in tags: + tags_by_instance_id[tag.res_id][tag.name] = tag.value + for reservation in reservations: for instance in reservation.instances: + instance.tags = tags_by_instance_id[instance.id] self.add_instance(instance, region) - except boto.exception.BotoServerError, e: - if not self.eucalyptus: - print "Looks like AWS is down again:" - print e - sys.exit(1) + except boto.exception.BotoServerError as e: + if e.error_code == 'AuthFailure': + error = self.get_auth_error_message() + else: + backend = 'Eucalyptus' if self.eucalyptus else 'AWS' + error = "Error connecting to %s backend.\n%s" % (backend, e.message) + self.fail_with_error(error, 'getting EC2 instances') def get_rds_instances_by_region(self, region): ''' Makes an AWS API call to the list of RDS instances in a particular region ''' try: - conn = rds.connect_to_region(region) + conn = self.connect_to_aws(rds, region) if conn: - instances = conn.get_all_dbinstances() - for instance in instances: - self.add_rds_instance(instance, region) - except boto.exception.BotoServerError, e: + marker = None + while True: + instances = conn.get_all_dbinstances(marker=marker) + marker = instances.marker + for instance in instances: + self.add_rds_instance(instance, region) + if not marker: + break + except boto.exception.BotoServerError as e: + error = e.reason + + if e.error_code == 'AuthFailure': + error = self.get_auth_error_message() if not e.reason == "Forbidden": - print "Looks like AWS RDS is down: " - print e - sys.exit(1) + error = "Looks like AWS RDS is down:\n%s" % e.message + self.fail_with_error(error, 'getting RDS instances') + + def include_rds_clusters_by_region(self, region): + if not HAS_BOTO3: + self.fail_with_error("Working with RDS clusters requires boto3 - please install boto3 and try again", + "getting RDS clusters") + + client = ec2_utils.boto3_inventory_conn('client', 'rds', region, **self.credentials) + + marker, clusters = '', [] + while marker is not None: + resp = client.describe_db_clusters(Marker=marker) + clusters.extend(resp["DBClusters"]) + marker = resp.get('Marker', None) + + account_id = boto.connect_iam().get_user().arn.split(':')[4] + c_dict = {} + for c in clusters: + # remove these datetime objects as there is no serialisation to json + # currently in place and we don't need the data yet + if 'EarliestRestorableTime' in c: + del c['EarliestRestorableTime'] + if 'LatestRestorableTime' in c: + del c['LatestRestorableTime'] + + if self.ec2_instance_filters == {}: + matches_filter = True + else: + matches_filter = False - def get_instance(self, region, instance_id): - ''' Gets details about a specific instance ''' - if self.eucalyptus: - conn = boto.connect_euca(self.eucalyptus_host) - conn.APIVersion = '2010-08-31' + try: + # arn:aws:rds:::: + tags = client.list_tags_for_resource( + ResourceName='arn:aws:rds:' + region + ':' + account_id + ':cluster:' + c['DBClusterIdentifier']) + c['Tags'] = tags['TagList'] + + if self.ec2_instance_filters: + for filter_key, filter_values in self.ec2_instance_filters.items(): + # get AWS tag key e.g. tag:env will be 'env' + tag_name = filter_key.split(":", 1)[1] + # Filter values is a list (if you put multiple values for the same tag name) + matches_filter = any(d['Key'] == tag_name and d['Value'] in filter_values for d in c['Tags']) + + if matches_filter: + # it matches a filter, so stop looking for further matches + break + + except Exception as e: + if e.message.find('DBInstanceNotFound') >= 0: + # AWS RDS bug (2016-01-06) means deletion does not fully complete and leave an 'empty' cluster. + # Ignore errors when trying to find tags for these + pass + + # ignore empty clusters caused by AWS bug + if len(c['DBClusterMembers']) == 0: + continue + elif matches_filter: + c_dict[c['DBClusterIdentifier']] = c + + self.inventory['db_clusters'] = c_dict + + def get_elasticache_clusters_by_region(self, region): + ''' Makes an AWS API call to the list of ElastiCache clusters (with + nodes' info) in a particular region.''' + + # ElastiCache boto module doesn't provide a get_all_intances method, + # that's why we need to call describe directly (it would be called by + # the shorthand method anyway...) + try: + conn = self.connect_to_aws(elasticache, region) + if conn: + # show_cache_node_info = True + # because we also want nodes' information + response = conn.describe_cache_clusters(None, None, None, True) + + except boto.exception.BotoServerError as e: + error = e.reason + + if e.error_code == 'AuthFailure': + error = self.get_auth_error_message() + if not e.reason == "Forbidden": + error = "Looks like AWS ElastiCache is down:\n%s" % e.message + self.fail_with_error(error, 'getting ElastiCache clusters') + + try: + # Boto also doesn't provide wrapper classes to CacheClusters or + # CacheNodes. Because of that wo can't make use of the get_list + # method in the AWSQueryConnection. Let's do the work manually + clusters = response['DescribeCacheClustersResponse']['DescribeCacheClustersResult']['CacheClusters'] + + except KeyError as e: + error = "ElastiCache query to AWS failed (unexpected format)." + self.fail_with_error(error, 'getting ElastiCache clusters') + + for cluster in clusters: + self.add_elasticache_cluster(cluster, region) + + def get_elasticache_replication_groups_by_region(self, region): + ''' Makes an AWS API call to the list of ElastiCache replication groups + in a particular region.''' + + # ElastiCache boto module doesn't provide a get_all_intances method, + # that's why we need to call describe directly (it would be called by + # the shorthand method anyway...) + try: + conn = self.connect_to_aws(elasticache, region) + if conn: + response = conn.describe_replication_groups() + + except boto.exception.BotoServerError as e: + error = e.reason + + if e.error_code == 'AuthFailure': + error = self.get_auth_error_message() + if not e.reason == "Forbidden": + error = "Looks like AWS ElastiCache [Replication Groups] is down:\n%s" % e.message + self.fail_with_error(error, 'getting ElastiCache clusters') + + try: + # Boto also doesn't provide wrapper classes to ReplicationGroups + # Because of that wo can't make use of the get_list method in the + # AWSQueryConnection. Let's do the work manually + replication_groups = response['DescribeReplicationGroupsResponse']['DescribeReplicationGroupsResult']['ReplicationGroups'] + + except KeyError as e: + error = "ElastiCache [Replication Groups] query to AWS failed (unexpected format)." + self.fail_with_error(error, 'getting ElastiCache clusters') + + for replication_group in replication_groups: + self.add_elasticache_replication_group(replication_group, region) + + def get_auth_error_message(self): + ''' create an informative error message if there is an issue authenticating''' + errors = ["Authentication error retrieving ec2 inventory."] + if None in [os.environ.get('AWS_ACCESS_KEY_ID'), os.environ.get('AWS_SECRET_ACCESS_KEY')]: + errors.append(' - No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY environment vars found') else: - conn = ec2.connect_to_region(region) + errors.append(' - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment vars found but may not be correct') - # connect_to_region will fail "silently" by returning None if the region name is wrong or not supported - if conn is None: - print("region name: %s likely not supported, or AWS is down. connection to region failed." % region) - sys.exit(1) + boto_paths = ['/etc/boto.cfg', '~/.boto', '~/.aws/credentials'] + boto_config_found = list(p for p in boto_paths if os.path.isfile(os.path.expanduser(p))) + if len(boto_config_found) > 0: + errors.append(" - Boto configs found at '%s', but the credentials contained may not be correct" % ', '.join(boto_config_found)) + else: + errors.append(" - No Boto config found at any expected location '%s'" % ', '.join(boto_paths)) + + return '\n'.join(errors) + + def fail_with_error(self, err_msg, err_operation=None): + '''log an error to std err for ansible-playbook to consume and exit''' + if err_operation: + err_msg = 'ERROR: "{err_msg}", while: {err_operation}'.format( + err_msg=err_msg, err_operation=err_operation) + sys.stderr.write(err_msg) + sys.exit(1) + + def get_instance(self, region, instance_id): + conn = self.connect(region) reservations = conn.get_all_instances([instance_id]) for reservation in reservations: @@ -414,13 +758,13 @@ def add_instance(self, instance, region): ''' Adds an instance to the inventory and index, as long as it is addressable ''' - # Only want running instances unless all_instances is True - if not self.all_instances and instance.state != 'running': + # Only return instances with desired instance states + if instance.state not in self.ec2_instance_states: return # Select the best destination address if self.destination_format and self.destination_format_tags: - dest = self.destination_format.format(*[ getattr(instance, 'tags').get(tag, 'nil') for tag in self.destination_format_tags ]) + dest = self.destination_format.format(*[ getattr(instance, 'tags').get(tag, '') for tag in self.destination_format_tags ]) elif instance.subnet_id: dest = getattr(instance, self.vpc_destination_variable, None) if dest is None: @@ -434,32 +778,46 @@ def add_instance(self, instance, region): # Skip instances we cannot address (e.g. private VPC subnet) return + # Set the inventory name + hostname = None + if self.hostname_variable: + if self.hostname_variable.startswith('tag_'): + hostname = instance.tags.get(self.hostname_variable[4:], None) + else: + hostname = getattr(instance, self.hostname_variable) + + # If we can't get a nice hostname, use the destination address + if not hostname: + hostname = dest + else: + hostname = self.to_safe(hostname).lower() + # if we only want to include hosts that match a pattern, skip those that don't - if self.pattern_include and not self.pattern_include.match(dest): + if self.pattern_include and not self.pattern_include.match(hostname): return # if we need to exclude hosts that match a pattern, skip those - if self.pattern_exclude and self.pattern_exclude.match(dest): + if self.pattern_exclude and self.pattern_exclude.match(hostname): return # Add to index - self.index[dest] = [region, instance.id] + self.index[hostname] = [region, instance.id] # Inventory: Group by instance ID (always a group of 1) if self.group_by_instance_id: - self.inventory[instance.id] = [dest] + self.inventory[instance.id] = [hostname] if self.nested_groups: self.push_group(self.inventory, 'instances', instance.id) # Inventory: Group by region if self.group_by_region: - self.push(self.inventory, region, dest) + self.push(self.inventory, region, hostname) if self.nested_groups: self.push_group(self.inventory, 'regions', region) # Inventory: Group by availability zone if self.group_by_availability_zone: - self.push(self.inventory, instance.placement, dest) + self.push(self.inventory, instance.placement, hostname) if self.nested_groups: if self.group_by_region: self.push_group(self.inventory, region, instance.placement) @@ -468,28 +826,28 @@ def add_instance(self, instance, region): # Inventory: Group by Amazon Machine Image (AMI) ID if self.group_by_ami_id: ami_id = self.to_safe(instance.image_id) - self.push(self.inventory, ami_id, dest) + self.push(self.inventory, ami_id, hostname) if self.nested_groups: self.push_group(self.inventory, 'images', ami_id) # Inventory: Group by instance type if self.group_by_instance_type: type_name = self.to_safe('type_' + instance.instance_type) - self.push(self.inventory, type_name, dest) + self.push(self.inventory, type_name, hostname) if self.nested_groups: self.push_group(self.inventory, 'types', type_name) # Inventory: Group by key pair if self.group_by_key_pair and instance.key_name: key_name = self.to_safe('key_' + instance.key_name) - self.push(self.inventory, key_name, dest) + self.push(self.inventory, key_name, hostname) if self.nested_groups: self.push_group(self.inventory, 'keys', key_name) # Inventory: Group by VPC if self.group_by_vpc_id and instance.vpc_id: vpc_id_name = self.to_safe('vpc_id_' + instance.vpc_id) - self.push(self.inventory, vpc_id_name, dest) + self.push(self.inventory, vpc_id_name, hostname) if self.nested_groups: self.push_group(self.inventory, 'vpcs', vpc_id_name) @@ -498,41 +856,51 @@ def add_instance(self, instance, region): try: for group in instance.groups: key = self.to_safe("security_group_" + group.name) - self.push(self.inventory, key, dest) + self.push(self.inventory, key, hostname) if self.nested_groups: self.push_group(self.inventory, 'security_groups', key) except AttributeError: - print 'Package boto seems a bit older.' - print 'Please upgrade boto >= 2.3.0.' - sys.exit(1) + self.fail_with_error('\n'.join(['Package boto seems a bit older.', + 'Please upgrade boto >= 2.3.0.'])) # Inventory: Group by tag keys if self.group_by_tag_keys: - for k, v in instance.tags.iteritems(): - key = self.to_safe("tag_" + k + "=" + v) - self.push(self.inventory, key, dest) - if self.nested_groups: - self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k)) - self.push_group(self.inventory, self.to_safe("tag_" + k), key) + for k, v in instance.tags.items(): + if self.expand_csv_tags and v and ',' in v: + values = map(lambda x: x.strip(), v.split(',')) + else: + values = [v] + + for v in values: + if v: + key = self.to_safe("tag_" + k + "=" + v) + else: + key = self.to_safe("tag_" + k) + self.push(self.inventory, key, hostname) + if self.nested_groups: + self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k)) + if v: + self.push_group(self.inventory, self.to_safe("tag_" + k), key) # Inventory: Group by Route53 domain names if enabled if self.route53_enabled and self.group_by_route53_names: route53_names = self.get_instance_route53_names(instance) for name in route53_names: - self.push(self.inventory, name, dest) + self.push(self.inventory, name, hostname) if self.nested_groups: self.push_group(self.inventory, 'route53', name) # Global Tag: instances without tags if self.group_by_tag_none and len(instance.tags) == 0: - self.push(self.inventory, 'tag_none', dest) + self.push(self.inventory, 'tag_none', hostname) if self.nested_groups: self.push_group(self.inventory, 'tags', 'tag_none') # Global Tag: tag all EC2 instances - self.push(self.inventory, 'ec2', dest) + self.push(self.inventory, 'ec2', hostname) - self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance) + self.inventory["_meta"]["hostvars"][hostname] = self.get_host_info_dict_from_instance(instance) + self.inventory["_meta"]["hostvars"][hostname]['ansible_ssh_host'] = dest def add_rds_instance(self, instance, region): @@ -550,24 +918,38 @@ def add_rds_instance(self, instance, region): # Skip instances we cannot address (e.g. private VPC subnet) return + # Set the inventory name + hostname = None + if self.hostname_variable: + if self.hostname_variable.startswith('tag_'): + hostname = instance.tags.get(self.hostname_variable[4:], None) + else: + hostname = getattr(instance, self.hostname_variable) + + # If we can't get a nice hostname, use the destination address + if not hostname: + hostname = dest + + hostname = self.to_safe(hostname).lower() + # Add to index - self.index[dest] = [region, instance.id] + self.index[hostname] = [region, instance.id] # Inventory: Group by instance ID (always a group of 1) if self.group_by_instance_id: - self.inventory[instance.id] = [dest] + self.inventory[instance.id] = [hostname] if self.nested_groups: self.push_group(self.inventory, 'instances', instance.id) # Inventory: Group by region if self.group_by_region: - self.push(self.inventory, region, dest) + self.push(self.inventory, region, hostname) if self.nested_groups: self.push_group(self.inventory, 'regions', region) # Inventory: Group by availability zone if self.group_by_availability_zone: - self.push(self.inventory, instance.availability_zone, dest) + self.push(self.inventory, instance.availability_zone, hostname) if self.nested_groups: if self.group_by_region: self.push_group(self.inventory, region, instance.availability_zone) @@ -576,14 +958,14 @@ def add_rds_instance(self, instance, region): # Inventory: Group by instance type if self.group_by_instance_type: type_name = self.to_safe('type_' + instance.instance_class) - self.push(self.inventory, type_name, dest) + self.push(self.inventory, type_name, hostname) if self.nested_groups: self.push_group(self.inventory, 'types', type_name) # Inventory: Group by VPC if self.group_by_vpc_id and instance.subnet_group and instance.subnet_group.vpc_id: vpc_id_name = self.to_safe('vpc_id_' + instance.subnet_group.vpc_id) - self.push(self.inventory, vpc_id_name, dest) + self.push(self.inventory, vpc_id_name, hostname) if self.nested_groups: self.push_group(self.inventory, 'vpcs', vpc_id_name) @@ -592,32 +974,270 @@ def add_rds_instance(self, instance, region): try: if instance.security_group: key = self.to_safe("security_group_" + instance.security_group.name) - self.push(self.inventory, key, dest) + self.push(self.inventory, key, hostname) if self.nested_groups: self.push_group(self.inventory, 'security_groups', key) except AttributeError: - print 'Package boto seems a bit older.' - print 'Please upgrade boto >= 2.3.0.' - sys.exit(1) + self.fail_with_error('\n'.join(['Package boto seems a bit older.', + 'Please upgrade boto >= 2.3.0.'])) + # Inventory: Group by engine if self.group_by_rds_engine: - self.push(self.inventory, self.to_safe("rds_" + instance.engine), dest) + self.push(self.inventory, self.to_safe("rds_" + instance.engine), hostname) if self.nested_groups: self.push_group(self.inventory, 'rds_engines', self.to_safe("rds_" + instance.engine)) # Inventory: Group by parameter group if self.group_by_rds_parameter_group: - self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), dest) + self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), hostname) if self.nested_groups: self.push_group(self.inventory, 'rds_parameter_groups', self.to_safe("rds_parameter_group_" + instance.parameter_group.name)) # Global Tag: all RDS instances - self.push(self.inventory, 'rds', dest) + self.push(self.inventory, 'rds', hostname) - self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance) + self.inventory["_meta"]["hostvars"][hostname] = self.get_host_info_dict_from_instance(instance) + self.inventory["_meta"]["hostvars"][hostname]['ansible_ssh_host'] = dest + def add_elasticache_cluster(self, cluster, region): + ''' Adds an ElastiCache cluster to the inventory and index, as long as + it's nodes are addressable ''' + + # Only want available clusters unless all_elasticache_clusters is True + if not self.all_elasticache_clusters and cluster['CacheClusterStatus'] != 'available': + return + + # Select the best destination address + if 'ConfigurationEndpoint' in cluster and cluster['ConfigurationEndpoint']: + # Memcached cluster + dest = cluster['ConfigurationEndpoint']['Address'] + is_redis = False + else: + # Redis sigle node cluster + # Because all Redis clusters are single nodes, we'll merge the + # info from the cluster with info about the node + dest = cluster['CacheNodes'][0]['Endpoint']['Address'] + is_redis = True + + if not dest: + # Skip clusters we cannot address (e.g. private VPC subnet) + return + + # Add to index + self.index[dest] = [region, cluster['CacheClusterId']] + + # Inventory: Group by instance ID (always a group of 1) + if self.group_by_instance_id: + self.inventory[cluster['CacheClusterId']] = [dest] + if self.nested_groups: + self.push_group(self.inventory, 'instances', cluster['CacheClusterId']) + + # Inventory: Group by region + if self.group_by_region and not is_redis: + self.push(self.inventory, region, dest) + if self.nested_groups: + self.push_group(self.inventory, 'regions', region) + + # Inventory: Group by availability zone + if self.group_by_availability_zone and not is_redis: + self.push(self.inventory, cluster['PreferredAvailabilityZone'], dest) + if self.nested_groups: + if self.group_by_region: + self.push_group(self.inventory, region, cluster['PreferredAvailabilityZone']) + self.push_group(self.inventory, 'zones', cluster['PreferredAvailabilityZone']) + + # Inventory: Group by node type + if self.group_by_instance_type and not is_redis: + type_name = self.to_safe('type_' + cluster['CacheNodeType']) + self.push(self.inventory, type_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'types', type_name) + + # Inventory: Group by VPC (information not available in the current + # AWS API version for ElastiCache) + + # Inventory: Group by security group + if self.group_by_security_group and not is_redis: + + # Check for the existence of the 'SecurityGroups' key and also if + # this key has some value. When the cluster is not placed in a SG + # the query can return None here and cause an error. + if 'SecurityGroups' in cluster and cluster['SecurityGroups'] is not None: + for security_group in cluster['SecurityGroups']: + key = self.to_safe("security_group_" + security_group['SecurityGroupId']) + self.push(self.inventory, key, dest) + if self.nested_groups: + self.push_group(self.inventory, 'security_groups', key) + + # Inventory: Group by engine + if self.group_by_elasticache_engine and not is_redis: + self.push(self.inventory, self.to_safe("elasticache_" + cluster['Engine']), dest) + if self.nested_groups: + self.push_group(self.inventory, 'elasticache_engines', self.to_safe(cluster['Engine'])) + + # Inventory: Group by parameter group + if self.group_by_elasticache_parameter_group: + self.push(self.inventory, self.to_safe("elasticache_parameter_group_" + cluster['CacheParameterGroup']['CacheParameterGroupName']), dest) + if self.nested_groups: + self.push_group(self.inventory, 'elasticache_parameter_groups', self.to_safe(cluster['CacheParameterGroup']['CacheParameterGroupName'])) + + # Inventory: Group by replication group + if self.group_by_elasticache_replication_group and 'ReplicationGroupId' in cluster and cluster['ReplicationGroupId']: + self.push(self.inventory, self.to_safe("elasticache_replication_group_" + cluster['ReplicationGroupId']), dest) + if self.nested_groups: + self.push_group(self.inventory, 'elasticache_replication_groups', self.to_safe(cluster['ReplicationGroupId'])) + + # Global Tag: all ElastiCache clusters + self.push(self.inventory, 'elasticache_clusters', cluster['CacheClusterId']) + + host_info = self.get_host_info_dict_from_describe_dict(cluster) + + self.inventory["_meta"]["hostvars"][dest] = host_info + + # Add the nodes + for node in cluster['CacheNodes']: + self.add_elasticache_node(node, cluster, region) + + def add_elasticache_node(self, node, cluster, region): + ''' Adds an ElastiCache node to the inventory and index, as long as + it is addressable ''' + + # Only want available nodes unless all_elasticache_nodes is True + if not self.all_elasticache_nodes and node['CacheNodeStatus'] != 'available': + return + + # Select the best destination address + dest = node['Endpoint']['Address'] + + if not dest: + # Skip nodes we cannot address (e.g. private VPC subnet) + return + + node_id = self.to_safe(cluster['CacheClusterId'] + '_' + node['CacheNodeId']) + + # Add to index + self.index[dest] = [region, node_id] + + # Inventory: Group by node ID (always a group of 1) + if self.group_by_instance_id: + self.inventory[node_id] = [dest] + if self.nested_groups: + self.push_group(self.inventory, 'instances', node_id) + + # Inventory: Group by region + if self.group_by_region: + self.push(self.inventory, region, dest) + if self.nested_groups: + self.push_group(self.inventory, 'regions', region) + + # Inventory: Group by availability zone + if self.group_by_availability_zone: + self.push(self.inventory, cluster['PreferredAvailabilityZone'], dest) + if self.nested_groups: + if self.group_by_region: + self.push_group(self.inventory, region, cluster['PreferredAvailabilityZone']) + self.push_group(self.inventory, 'zones', cluster['PreferredAvailabilityZone']) + + # Inventory: Group by node type + if self.group_by_instance_type: + type_name = self.to_safe('type_' + cluster['CacheNodeType']) + self.push(self.inventory, type_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'types', type_name) + + # Inventory: Group by VPC (information not available in the current + # AWS API version for ElastiCache) + + # Inventory: Group by security group + if self.group_by_security_group: + + # Check for the existence of the 'SecurityGroups' key and also if + # this key has some value. When the cluster is not placed in a SG + # the query can return None here and cause an error. + if 'SecurityGroups' in cluster and cluster['SecurityGroups'] is not None: + for security_group in cluster['SecurityGroups']: + key = self.to_safe("security_group_" + security_group['SecurityGroupId']) + self.push(self.inventory, key, dest) + if self.nested_groups: + self.push_group(self.inventory, 'security_groups', key) + + # Inventory: Group by engine + if self.group_by_elasticache_engine: + self.push(self.inventory, self.to_safe("elasticache_" + cluster['Engine']), dest) + if self.nested_groups: + self.push_group(self.inventory, 'elasticache_engines', self.to_safe("elasticache_" + cluster['Engine'])) + + # Inventory: Group by parameter group (done at cluster level) + + # Inventory: Group by replication group (done at cluster level) + + # Inventory: Group by ElastiCache Cluster + if self.group_by_elasticache_cluster: + self.push(self.inventory, self.to_safe("elasticache_cluster_" + cluster['CacheClusterId']), dest) + + # Global Tag: all ElastiCache nodes + self.push(self.inventory, 'elasticache_nodes', dest) + + host_info = self.get_host_info_dict_from_describe_dict(node) + + if dest in self.inventory["_meta"]["hostvars"]: + self.inventory["_meta"]["hostvars"][dest].update(host_info) + else: + self.inventory["_meta"]["hostvars"][dest] = host_info + + def add_elasticache_replication_group(self, replication_group, region): + ''' Adds an ElastiCache replication group to the inventory and index ''' + + # Only want available clusters unless all_elasticache_replication_groups is True + if not self.all_elasticache_replication_groups and replication_group['Status'] != 'available': + return + + # Select the best destination address (PrimaryEndpoint) + dest = replication_group['NodeGroups'][0]['PrimaryEndpoint']['Address'] + + if not dest: + # Skip clusters we cannot address (e.g. private VPC subnet) + return + + # Add to index + self.index[dest] = [region, replication_group['ReplicationGroupId']] + + # Inventory: Group by ID (always a group of 1) + if self.group_by_instance_id: + self.inventory[replication_group['ReplicationGroupId']] = [dest] + if self.nested_groups: + self.push_group(self.inventory, 'instances', replication_group['ReplicationGroupId']) + + # Inventory: Group by region + if self.group_by_region: + self.push(self.inventory, region, dest) + if self.nested_groups: + self.push_group(self.inventory, 'regions', region) + + # Inventory: Group by availability zone (doesn't apply to replication groups) + + # Inventory: Group by node type (doesn't apply to replication groups) + + # Inventory: Group by VPC (information not available in the current + # AWS API version for replication groups + + # Inventory: Group by security group (doesn't apply to replication groups) + # Check this value in cluster level + + # Inventory: Group by engine (replication groups are always Redis) + if self.group_by_elasticache_engine: + self.push(self.inventory, 'elasticache_redis', dest) + if self.nested_groups: + self.push_group(self.inventory, 'elasticache_engines', 'redis') + + # Global Tag: all ElastiCache clusters + self.push(self.inventory, 'elasticache_replication_groups', replication_group['ReplicationGroupId']) + + host_info = self.get_host_info_dict_from_describe_dict(replication_group) + + self.inventory["_meta"]["hostvars"][dest] = host_info def get_route53_records(self): ''' Get and store the map of resource records to domain names that @@ -666,7 +1286,6 @@ def get_instance_route53_names(self, instance): return list(name_list) - def get_host_info_dict_from_instance(self, instance): instance_vars = {} for key in vars(instance): @@ -683,7 +1302,7 @@ def get_host_info_dict_from_instance(self, instance): instance_vars['ec2_previous_state_code'] = instance.previous_state_code elif type(value) in [int, bool]: instance_vars[key] = value - elif type(value) in [str, unicode]: + elif isinstance(value, six.string_types): instance_vars[key] = value.strip() elif type(value) == type(None): instance_vars[key] = '' @@ -692,7 +1311,9 @@ def get_host_info_dict_from_instance(self, instance): elif key == 'ec2__placement': instance_vars['ec2_placement'] = value.zone elif key == 'ec2_tags': - for k, v in value.iteritems(): + for k, v in value.items(): + if self.expand_csv_tags and ',' in v: + v = map(lambda x: x.strip(), v.split(',')) key = self.to_safe('ec2_tag_' + k) instance_vars[key] = v elif key == 'ec2_groups': @@ -703,6 +1324,10 @@ def get_host_info_dict_from_instance(self, instance): group_names.append(group.name) instance_vars["ec2_security_group_ids"] = ','.join([str(i) for i in group_ids]) instance_vars["ec2_security_group_names"] = ','.join([str(i) for i in group_names]) + elif key == 'ec2_block_device_mapping': + instance_vars["ec2_block_devices"] = {} + for k, v in value.items(): + instance_vars["ec2_block_devices"][ os.path.basename(k) ] = v.volume_id else: pass # TODO Product codes if someone finds them useful @@ -710,11 +1335,93 @@ def get_host_info_dict_from_instance(self, instance): #print type(value) #print value - if self.route53_enabled: - instance_vars["ec2_route53_names"] = self.get_instance_route53_names(instance) - return instance_vars + def get_host_info_dict_from_describe_dict(self, describe_dict): + ''' Parses the dictionary returned by the API call into a flat list + of parameters. This method should be used only when 'describe' is + used directly because Boto doesn't provide specific classes. ''' + + # I really don't agree with prefixing everything with 'ec2' + # because EC2, RDS and ElastiCache are different services. + # I'm just following the pattern used until now to not break any + # compatibility. + + host_info = {} + for key in describe_dict: + value = describe_dict[key] + key = self.to_safe('ec2_' + self.uncammelize(key)) + + # Handle complex types + + # Target: Memcached Cache Clusters + if key == 'ec2_configuration_endpoint' and value: + host_info['ec2_configuration_endpoint_address'] = value['Address'] + host_info['ec2_configuration_endpoint_port'] = value['Port'] + + # Target: Cache Nodes and Redis Cache Clusters (single node) + if key == 'ec2_endpoint' and value: + host_info['ec2_endpoint_address'] = value['Address'] + host_info['ec2_endpoint_port'] = value['Port'] + + # Target: Redis Replication Groups + if key == 'ec2_node_groups' and value: + host_info['ec2_endpoint_address'] = value[0]['PrimaryEndpoint']['Address'] + host_info['ec2_endpoint_port'] = value[0]['PrimaryEndpoint']['Port'] + replica_count = 0 + for node in value[0]['NodeGroupMembers']: + if node['CurrentRole'] == 'primary': + host_info['ec2_primary_cluster_address'] = node['ReadEndpoint']['Address'] + host_info['ec2_primary_cluster_port'] = node['ReadEndpoint']['Port'] + host_info['ec2_primary_cluster_id'] = node['CacheClusterId'] + elif node['CurrentRole'] == 'replica': + host_info['ec2_replica_cluster_address_'+ str(replica_count)] = node['ReadEndpoint']['Address'] + host_info['ec2_replica_cluster_port_'+ str(replica_count)] = node['ReadEndpoint']['Port'] + host_info['ec2_replica_cluster_id_'+ str(replica_count)] = node['CacheClusterId'] + replica_count += 1 + + # Target: Redis Replication Groups + if key == 'ec2_member_clusters' and value: + host_info['ec2_member_clusters'] = ','.join([str(i) for i in value]) + + # Target: All Cache Clusters + elif key == 'ec2_cache_parameter_group': + host_info["ec2_cache_node_ids_to_reboot"] = ','.join([str(i) for i in value['CacheNodeIdsToReboot']]) + host_info['ec2_cache_parameter_group_name'] = value['CacheParameterGroupName'] + host_info['ec2_cache_parameter_apply_status'] = value['ParameterApplyStatus'] + + # Target: Almost everything + elif key == 'ec2_security_groups': + + # Skip if SecurityGroups is None + # (it is possible to have the key defined but no value in it). + if value is not None: + sg_ids = [] + for sg in value: + sg_ids.append(sg['SecurityGroupId']) + host_info["ec2_security_group_ids"] = ','.join([str(i) for i in sg_ids]) + + # Target: Everything + # Preserve booleans and integers + elif type(value) in [int, bool]: + host_info[key] = value + + # Target: Everything + # Sanitize string values + elif isinstance(value, six.string_types): + host_info[key] = value.strip() + + # Target: Everything + # Replace None by an empty string + elif type(value) == type(None): + host_info[key] = '' + + else: + # Remove non-processed complex types + pass + + return host_info + def get_host_info(self): ''' Get variables about a specific host ''' @@ -778,13 +1485,16 @@ def write_to_cache(self, data, filename): cache.write(json_data) cache.close() + def uncammelize(self, key): + temp = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', key) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', temp).lower() def to_safe(self, word): - ''' Converts 'bad' characters in a string to underscores so they can be - used as Ansible groups ''' - - return re.sub("[^A-Za-z0-9\-]", "_", word) - + ''' Converts 'bad' characters in a string to underscores so they can be used as Ansible groups ''' + regex = "[^A-Za-z0-9\_" + if not self.replace_dash_in_groups: + regex += "\-" + return re.sub(regex + "]", "_", word) def json_format_dict(self, data, pretty=False): ''' Converts a dict to a JSON object and dumps it as a formatted @@ -798,4 +1508,3 @@ def json_format_dict(self, data, pretty=False): # Run the script Ec2Inventory() - diff --git a/reference-architecture/aws-ansible/playbooks/add-node.yaml b/reference-architecture/aws-ansible/playbooks/add-node.yaml new file mode 100644 index 000000000..848715333 --- /dev/null +++ b/reference-architecture/aws-ansible/playbooks/add-node.yaml @@ -0,0 +1,73 @@ +- hosts: localhost + connection: local + gather_facts: no + become: no + vars_files: + - vars/main.yaml + roles: + # Group systems + - instance-groups + +- hosts: new_nodes + gather_facts: yes + become: yes + serial: 1 + user: ec2-user + vars_files: + - vars/main.yaml + roles: + - rhsm-subscription + +- hosts: new_nodes + gather_facts: no + become: yes + user: ec2-user + vars_files: + - vars/main.yaml + roles: + - rhsm-repos + - prerequisites + +- include: node-setup.yaml + +- hosts: primary_master + gather_facts: yes + become: yes + vars_files: + - vars/main.yaml + roles: + - infra-node-scaleup + +- hosts: new_nodes + gather_facts: no + become: yes + vars_files: + - vars/main.yaml + roles: + - openshift-emptydir-quota + +- hosts: localhost + connection: local + gather_facts: no + become: no + vars_files: + - vars/main.yaml + post_tasks: + - name: add instance to ELB if node is infra + ec2_elb: + instance_id: "{{ hostvars[item].ec2_id }}" + ec2_elbs: "{{ infra_elb_name }}" + region: "{{ region }}" + wait: no + state: present + with_items: "{{ groups['tag_provision_node'] }}" + when: node_type == "infra" + + - name: tag a resource + ec2_tag: + region: "{{ region }}" + resource: "{{ hostvars[item].ec2_id }}" + state: absent + tags: + provision: node + with_items: "{{ groups['tag_provision_node'] }}" diff --git a/reference-architecture/aws-ansible/playbooks/node-setup.yaml b/reference-architecture/aws-ansible/playbooks/node-setup.yaml new file mode 100644 index 000000000..978c62c26 --- /dev/null +++ b/reference-architecture/aws-ansible/playbooks/node-setup.yaml @@ -0,0 +1,44 @@ +- include: /usr/share/ansible/openshift-ansible/playbooks/byo/openshift-node/scaleup.yml + vars: + debug_level: 2 + openshift_debug_level: "{{ debug_level }}" + openshift_node_debug_level: "{{ node_debug_level | default(debug_level, true) }}" + osm_controller_args: + cloud-provider: + - "aws" + osm_api_server_args: + cloud-provider: + - "aws" + openshift_node_kubelet_args: + cloud-provider: + - "aws" + node-labels: + - "role={{ openshift_node_labels.role }}" + openshift_master_debug_level: "{{ master_debug_level | default(debug_level, true) }}" + openshift_master_access_token_max_seconds: 2419200 + openshift_master_api_port: "{{ console_port }}" + openshift_master_console_port: "{{ console_port }}" + osm_cluster_network_cidr: 172.16.0.0/16 + openshift_registry_selector: "role=infra" + openshift_router_selector: "role=infra" + openshift_hosted_router_replicas: 2 + openshift_hosted_registry_replicas: 2 + openshift_master_cluster_method: native + openshift_cloudprovider_kind: aws + openshift_master_cluster_hostname: "internal-openshift-master.{{ public_hosted_zone }}" + openshift_master_cluster_public_hostname: "openshift-master.{{ public_hosted_zone }}" + osm_default_subdomain: "{{ wildcard_zone }}" + osm_default_node_selector: "role=app" + deployment_type: openshift-enterprise + openshift_master_identity_providers: + - name: github + kind: GitHubIdentityProvider + login: true + challenge: false + mapping_method: claim + clientID: e76865557b0417387b35 + clientSecret: 72b36e28221c1b93089ecf72f1a19963a8532b06 + organizations: + - openshift + osm_use_cockpit: false + containerized: false diff --git a/reference-architecture/aws-ansible/playbooks/openshift-install.yaml b/reference-architecture/aws-ansible/playbooks/openshift-install.yaml index 978aa7f97..4553146ad 100644 --- a/reference-architecture/aws-ansible/playbooks/openshift-install.yaml +++ b/reference-architecture/aws-ansible/playbooks/openshift-install.yaml @@ -8,10 +8,10 @@ # Group systems - instance-groups -- hosts: bastion +- hosts: localhost + connection: local gather_facts: no - become: yes - user: ec2-user + become: no vars_files: - vars/main.yaml roles: diff --git a/reference-architecture/aws-ansible/playbooks/openshift-setup.yaml b/reference-architecture/aws-ansible/playbooks/openshift-setup.yaml index f8bd92308..57941e27f 100644 --- a/reference-architecture/aws-ansible/playbooks/openshift-setup.yaml +++ b/reference-architecture/aws-ansible/playbooks/openshift-setup.yaml @@ -37,7 +37,7 @@ challenge: false mapping_method: claim clientID: e76865557b0417387b35 - clientSecret: a2439464495d6b579d25f46dd51eb05a170e7e59 + clientSecret: 72b36e28221c1b93089ecf72f1a19963a8532b06 organizations: - openshift osm_use_cockpit: false diff --git a/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/files/add-infra-node.json b/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/files/add-infra-node.json new file mode 100644 index 000000000..d740802a0 --- /dev/null +++ b/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/files/add-infra-node.json @@ -0,0 +1,131 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": { + "KeyName": { + "Type": "AWS::EC2::KeyPair::KeyName" + }, + "Route53HostedZone": { + "Type": "String" + }, + "AmiId": { + "Type": "AWS::EC2::Image::Id" + }, + "InstanceType": { + "Type": "String", + "Default": "t2.medium" + }, + "NodeRootVolSize": { + "Type": "String", + "Default": "30" + }, + "NodeDockerVolSize": { + "Type": "String", + "Default": "25" + }, + "NodeDockerVolType": { + "Type": "String", + "Default": "gp2" + }, + "NodeUserData": { + "Type": "String" + }, + "NodeEmptyVolSize": { + "Type": "String", + "Default": "25" + }, + "NodeEmptyVolType": { + "Type": "String", + "Default": "gp2" + }, + "NodeRootVolType": { + "Type": "String", + "Default": "gp2" + }, + "Subnet": { + "Type": "String" + }, + "NodeName": { + "Type": "String" + }, + "NodeInstanceProfile": { + "Type": "String" + }, + "NodeType": { + "Type": "String" + }, + "InfraSg": { + "Type": "String" + }, + "NodeSg": { + "Type": "String" + } + }, + "Resources": { + "Route53Records": { + "Type": "AWS::Route53::RecordSetGroup", + "DependsOn": [ + "NewNode" + ], + "Properties": { + "HostedZoneName": { "Ref": "Route53HostedZone" }, + "RecordSets": [ + { + "Name": { "Ref": "NodeName" }, + "Type": "A", + "TTL": "300", + "ResourceRecords": [{ "Fn::GetAtt" : ["NewNode", "PrivateIp"] }] + } + ] + } + }, + "NewNode" : { + "Type" : "AWS::EC2::Instance", + "Properties" : { + "ImageId" : {"Ref": "AmiId"}, + "UserData": {"Ref": "NodeUserData"}, + "KeyName" : {"Ref": "KeyName"}, + "InstanceType": {"Ref": "InstanceType"}, + "SecurityGroupIds": [{"Ref": "NodeSg"}, {"Ref": "InfraSg"}], + "SubnetId" : {"Ref": "Subnet"}, + "IamInstanceProfile": { "Ref": "NodeInstanceProfile" }, + "Tags": [ + { "Key": "Name", + "Value": {"Ref": "NodeName"} + }, + { "Key": "provision", + "Value": "node" + }, + { "Key": "openshift-role", + "Value": {"Ref": "NodeType"} + } + ], + "BlockDeviceMappings" : [ + { + "DeviceName": "/dev/sda1", + "Ebs": { + "DeleteOnTermination": "true", + "VolumeSize": {"Ref": "NodeRootVolSize"}, + "VolumeType": {"Ref": "NodeRootVolType"} + } + }, + { + "DeviceName": "/dev/xvdb", + "Ebs": { + "DeleteOnTermination": "true", + "VolumeSize": {"Ref": "NodeDockerVolSize"}, + "VolumeType": {"Ref": "NodeDockerVolType"} + } + }, + { + "DeviceName": "/dev/xvdc", + "Ebs": { + "DeleteOnTermination": "true", + "VolumeSize": {"Ref": "NodeEmptyVolSize"}, + "VolumeType": {"Ref": "NodeEmptyVolType"} + } + } + ] + } + } + } +} diff --git a/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/files/add-node.json b/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/files/add-node.json new file mode 100644 index 000000000..5e916f7c1 --- /dev/null +++ b/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/files/add-node.json @@ -0,0 +1,128 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": { + "KeyName": { + "Type": "AWS::EC2::KeyPair::KeyName" + }, + "Route53HostedZone": { + "Type": "String" + }, + "AmiId": { + "Type": "AWS::EC2::Image::Id" + }, + "InstanceType": { + "Type": "String", + "Default": "t2.medium" + }, + "NodeRootVolSize": { + "Type": "String", + "Default": "30" + }, + "NodeDockerVolSize": { + "Type": "String", + "Default": "25" + }, + "NodeDockerVolType": { + "Type": "String", + "Default": "gp2" + }, + "NodeUserData": { + "Type": "String" + }, + "NodeEmptyVolSize": { + "Type": "String", + "Default": "25" + }, + "NodeEmptyVolType": { + "Type": "String", + "Default": "gp2" + }, + "NodeRootVolType": { + "Type": "String", + "Default": "gp2" + }, + "Subnet": { + "Type": "String" + }, + "NodeName": { + "Type": "String" + }, + "NodeInstanceProfile": { + "Type": "String" + }, + "NodeType": { + "Type": "String" + }, + "NodeSg": { + "Type": "String" + } + }, + "Resources": { + "Route53Records": { + "Type": "AWS::Route53::RecordSetGroup", + "DependsOn": [ + "NewNode" + ], + "Properties": { + "HostedZoneName": { "Ref": "Route53HostedZone" }, + "RecordSets": [ + { + "Name": { "Ref": "NodeName" }, + "Type": "A", + "TTL": "300", + "ResourceRecords": [{ "Fn::GetAtt" : ["NewNode", "PrivateIp"] }] + } + ] + } + }, + "NewNode" : { + "Type" : "AWS::EC2::Instance", + "Properties" : { + "ImageId" : {"Ref": "AmiId"}, + "UserData": {"Ref": "NodeUserData"}, + "KeyName" : {"Ref": "KeyName"}, + "InstanceType": {"Ref": "InstanceType"}, + "SecurityGroupIds": [{"Ref": "NodeSg"}], + "SubnetId" : {"Ref": "Subnet"}, + "IamInstanceProfile": { "Ref": "NodeInstanceProfile" }, + "Tags": [ + { "Key": "Name", + "Value": {"Ref": "NodeName"} + }, + { "Key": "provision", + "Value": "node" + }, + { "Key": "openshift-role", + "Value": {"Ref": "NodeType"} + } + ], + "BlockDeviceMappings" : [ + { + "DeviceName": "/dev/sda1", + "Ebs": { + "DeleteOnTermination": "true", + "VolumeSize": {"Ref": "NodeRootVolSize"}, + "VolumeType": {"Ref": "NodeRootVolType"} + } + }, + { + "DeviceName": "/dev/xvdb", + "Ebs": { + "DeleteOnTermination": "true", + "VolumeSize": {"Ref": "NodeDockerVolSize"}, + "VolumeType": {"Ref": "NodeDockerVolType"} + } + }, + { + "DeviceName": "/dev/xvdc", + "Ebs": { + "DeleteOnTermination": "true", + "VolumeSize": {"Ref": "NodeEmptyVolSize"}, + "VolumeType": {"Ref": "NodeEmptyVolType"} + } + } + ] + } + } + } +} diff --git a/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/files/greenfield.json b/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/files/greenfield.json index f76323427..bdba27d06 100644 --- a/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/files/greenfield.json +++ b/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/files/greenfield.json @@ -1010,8 +1010,8 @@ "ImageId" : {"Ref": "AmiId"}, "UserData": {"Ref": "MasterUserData"}, "KeyName" : {"Ref": "KeyName"}, - "InstanceType": {"Ref": "MasterInstanceType"}, - "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }, { "Fn::GetAtt" : ["MasterSG", "GroupId"] }, { "Fn::GetAtt" : ["EtcdSG", "GroupId"] }], + "InstanceType": {"Ref": "MasterInstanceType"}, + "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }, { "Fn::GetAtt" : ["MasterSG", "GroupId"] }, { "Fn::GetAtt" : ["EtcdSG", "GroupId"] }], "SubnetId" : {"Ref": "PrivateSubnet2"}, "IamInstanceProfile": { "Ref": "MasterInstanceProfile" }, "Tags": [ @@ -1057,8 +1057,8 @@ "ImageId" : {"Ref": "AmiId"}, "UserData": {"Ref": "MasterUserData"}, "KeyName" : {"Ref": "KeyName"}, - "InstanceType": {"Ref": "MasterInstanceType"}, - "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }, { "Fn::GetAtt" : ["MasterSG", "GroupId"] }, { "Fn::GetAtt" : ["EtcdSG", "GroupId"] }], + "InstanceType": {"Ref": "MasterInstanceType"}, + "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }, { "Fn::GetAtt" : ["MasterSG", "GroupId"] }, { "Fn::GetAtt" : ["EtcdSG", "GroupId"] }], "SubnetId" : {"Ref": "PrivateSubnet3"}, "IamInstanceProfile": { "Ref": "MasterInstanceProfile" }, "Tags": [ @@ -1104,10 +1104,10 @@ "ImageId" : {"Ref": "AmiId"}, "UserData": {"Ref": "NodeUserData"}, "KeyName" : {"Ref": "KeyName"}, - "InstanceType": {"Ref": "InfraInstanceType"}, - "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }, { "Fn::GetAtt" : ["InfraSG", "GroupId"] }], + "InstanceType": {"Ref": "InfraInstanceType"}, + "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }, { "Fn::GetAtt" : ["InfraSG", "GroupId"] }], "SubnetId" : {"Ref": "PrivateSubnet1"}, - "IamInstanceProfile": { "Ref": "NodeInstanceProfile" }, + "IamInstanceProfile": { "Ref": "NodeInstanceProfile" }, "Tags": [ { "Key": "Name", "Value": {"Fn::Join": [".", ["ose-infra-node01",{"Ref": "PublicHostedZone"}]]} @@ -1151,10 +1151,10 @@ "ImageId" : {"Ref": "AmiId"}, "UserData": {"Ref": "NodeUserData"}, "KeyName" : {"Ref": "KeyName"}, - "InstanceType": {"Ref": "InfraInstanceType"}, - "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }, { "Fn::GetAtt" : ["InfraSG", "GroupId"] }], + "InstanceType": {"Ref": "InfraInstanceType"}, + "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }, { "Fn::GetAtt" : ["InfraSG", "GroupId"] }], "SubnetId" : {"Ref": "PrivateSubnet2"}, - "IamInstanceProfile": { "Ref": "NodeInstanceProfile" }, + "IamInstanceProfile": { "Ref": "NodeInstanceProfile" }, "Tags": [ { "Key": "Name", "Value": {"Fn::Join": [".", ["ose-infra-node02",{"Ref": "PublicHostedZone"}]]} @@ -1198,10 +1198,10 @@ "ImageId" : {"Ref": "AmiId"}, "UserData": {"Ref": "NodeUserData"}, "KeyName" : {"Ref": "KeyName"}, - "InstanceType": {"Ref": "AppNodeInstanceType"}, - "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }], + "InstanceType": {"Ref": "AppNodeInstanceType"}, + "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }], "SubnetId" : {"Ref": "PrivateSubnet1"}, - "IamInstanceProfile": { "Ref": "NodeInstanceProfile" }, + "IamInstanceProfile": { "Ref": "NodeInstanceProfile" }, "Tags": [ { "Key": "Name", "Value": {"Fn::Join": [".", ["ose-app-node01",{"Ref": "PublicHostedZone"}]]} @@ -1245,10 +1245,10 @@ "ImageId" : {"Ref": "AmiId"}, "UserData": {"Ref": "NodeUserData"}, "KeyName" : {"Ref": "KeyName"}, - "InstanceType": {"Ref": "AppNodeInstanceType"}, - "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }], + "InstanceType": {"Ref": "AppNodeInstanceType"}, + "SecurityGroupIds": [{ "Fn::GetAtt" : ["NodeSG", "GroupId"] }], "SubnetId" : {"Ref": "PrivateSubnet2"}, - "IamInstanceProfile": { "Ref": "NodeInstanceProfile" }, + "IamInstanceProfile": { "Ref": "NodeInstanceProfile" }, "Tags": [ { "Key": "Name", "Value": {"Fn::Join": [".", ["ose-app-node02",{"Ref": "PublicHostedZone"}]]} diff --git a/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/tasks/main.yaml b/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/tasks/main.yaml index 28242601e..42c66da41 100644 --- a/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/tasks/main.yaml +++ b/reference-architecture/aws-ansible/playbooks/roles/cloudformation-infra/tasks/main.yaml @@ -141,3 +141,54 @@ NodeDockerVolSize: "{{ docker_storage }}" NodeDockerVolType: gp2 when: create_vpc == "no" and byo_bastion == "yes" + +- name: Add App Node + cloudformation: + stack_name: "{{ shortname }}" + state: "present" + region: "{{ region }}" + template: "roles/cloudformation-infra/files/add-node.json" + template_parameters: + NodeName: "{{ fqdn }}" + NodeSg: "{{ node_sg }}" + Subnet: "{{ subnet_id }}" + Route53HostedZone: "{{ public_hosted_zone }}." + KeyName: "{{ keypair }}" + AmiId: "{{ ami }}" + InstanceType: "{{ node_instance_type }}" + NodeEmptyVolSize: "{{ emptydir_storage }}" + NodeEmptyVolType: gp2 + NodeUserData: "{{ lookup('file', 'user_data_node.yml') | b64encode }}" + NodeRootVolSize: 25 + NodeRootVolType: gp2 + NodeDockerVolSize: "{{ docker_storage }}" + NodeDockerVolType: gp2 + NodeInstanceProfile: "{{ iam_role }}" + NodeType: "{{ node_type }}" + when: create_vpc == "no" and add_node == "yes" and node_type == "app" + +- name: Add Infra Node + cloudformation: + stack_name: "{{ shortname }}" + state: "present" + region: "{{ region }}" + template: "roles/cloudformation-infra/files/add-infra-node.json" + template_parameters: + NodeName: "{{ fqdn }}" + NodeSg: "{{ node_sg }}" + InfraSg: "{{ infra_sg }}" + Subnet: "{{ subnet_id }}" + Route53HostedZone: "{{ public_hosted_zone }}." + KeyName: "{{ keypair }}" + AmiId: "{{ ami }}" + InstanceType: "{{ node_instance_type }}" + NodeEmptyVolSize: "{{ emptydir_storage }}" + NodeEmptyVolType: gp2 + NodeUserData: "{{ lookup('file', 'user_data_node.yml') | b64encode }}" + NodeRootVolSize: 25 + NodeRootVolType: gp2 + NodeDockerVolSize: "{{ docker_storage }}" + NodeDockerVolType: gp2 + NodeInstanceProfile: "{{ iam_role }}" + NodeType: "{{ node_type }}" + when: create_vpc == "no" and add_node == "yes" and node_type == "infra" diff --git a/reference-architecture/aws-ansible/playbooks/roles/docker-storage-setup/files/docker-storage-setup b/reference-architecture/aws-ansible/playbooks/roles/docker-storage-setup/files/docker-storage-setup deleted file mode 100644 index 66269fdc4..000000000 --- a/reference-architecture/aws-ansible/playbooks/roles/docker-storage-setup/files/docker-storage-setup +++ /dev/null @@ -1,4 +0,0 @@ -DEVS=/dev/xvdb -VG=docker-vol -DATA_SIZE=95%VG -EXTRA_DOCKER_STORAGE_OPTIONS="--storage-opt dm.basesize=3G" diff --git a/reference-architecture/aws-ansible/playbooks/roles/docker-storage-setup/tasks/main.yaml b/reference-architecture/aws-ansible/playbooks/roles/docker-storage-setup/tasks/main.yaml deleted file mode 100644 index 4b082391b..000000000 --- a/reference-architecture/aws-ansible/playbooks/roles/docker-storage-setup/tasks/main.yaml +++ /dev/null @@ -1,13 +0,0 @@ ---- -- name: create the docker-storage-setup config file - copy: - src: docker-storage-setup - dest: /etc/sysconfig/docker-storage-setup - owner: root - group: root - mode: 0644 -- name: start docker - service: - name: docker - state: started - enabled: true diff --git a/reference-architecture/aws-ansible/playbooks/roles/host-up/tasks/main.yaml b/reference-architecture/aws-ansible/playbooks/roles/host-up/tasks/main.yaml index e8ff6f237..ee90e7cbb 100644 --- a/reference-architecture/aws-ansible/playbooks/roles/host-up/tasks/main.yaml +++ b/reference-architecture/aws-ansible/playbooks/roles/host-up/tasks/main.yaml @@ -2,6 +2,7 @@ - name: check to see if host is available wait_for: port: 22 + host: "bastion.{{ public_hosted_zone }}" state: started delay: 20 - when: "'bastion' in inventory_hostname" + when: byo_bastion == "no" diff --git a/reference-architecture/aws-ansible/playbooks/roles/infra-node-scaleup/library/openshift_facts.py b/reference-architecture/aws-ansible/playbooks/roles/infra-node-scaleup/library/openshift_facts.py new file mode 120000 index 000000000..e0061bb77 --- /dev/null +++ b/reference-architecture/aws-ansible/playbooks/roles/infra-node-scaleup/library/openshift_facts.py @@ -0,0 +1 @@ +/usr/share/ansible/openshift-ansible/roles/openshift_facts/library/openshift_facts.py \ No newline at end of file diff --git a/reference-architecture/aws-ansible/playbooks/roles/infra-node-scaleup/tasks/main.yaml b/reference-architecture/aws-ansible/playbooks/roles/infra-node-scaleup/tasks/main.yaml new file mode 100644 index 000000000..a44abac26 --- /dev/null +++ b/reference-architecture/aws-ansible/playbooks/roles/infra-node-scaleup/tasks/main.yaml @@ -0,0 +1,20 @@ +--- +- name: Gather facts + openshift_facts: + role: common + +- name: use the default project + shell: "{{ openshift.common.client_binary }} project default" + +- name: Count the infrastructure nodes + shell: "{{ openshift.common.client_binary }} get nodes --show-labels | grep role=infra | wc -l" + register: nodes + when: node_type == "infra" + +- name: Scale the router + shell: "{{ openshift.common.client_binary }} scale dc/router --replicas={{ nodes.stdout }}" + when: node_type == "infra" + +- name: Scale the registry + shell: "{{ openshift.common.client_binary }} scale dc/docker-registry --replicas={{ nodes.stdout }}" + when: node_type == "infra" diff --git a/reference-architecture/aws-ansible/playbooks/roles/instance-groups/tasks/main.yaml b/reference-architecture/aws-ansible/playbooks/roles/instance-groups/tasks/main.yaml index 7f7fe4d06..160162cad 100644 --- a/reference-architecture/aws-ansible/playbooks/roles/instance-groups/tasks/main.yaml +++ b/reference-architecture/aws-ansible/playbooks/roles/instance-groups/tasks/main.yaml @@ -11,7 +11,7 @@ groups: masters, etcd, nodes, cluster_hosts openshift_node_labels: role: master - with_items: "{{ groups['tag_openshift-role_master'] }}" + with_items: "{{ groups['tag_openshift_role_master'] }}" - name: Add a master to the primary masters group add_host: @@ -19,7 +19,7 @@ groups: primary_master openshift_node_labels: role: master - with_items: "{{ groups['tag_openshift-role_master'].0 }}" + with_items: "{{ groups['tag_openshift_role_master'].0 }}" - name: Add infra instances to host group add_host: @@ -27,7 +27,7 @@ groups: nodes, cluster_hosts, schedulable_nodes openshift_node_labels: role: infra - with_items: "{{ groups['tag_openshift-role_infra'] }}" + with_items: "{{ groups['tag_openshift_role_infra'] }}" - name: Add app instances to host group add_host: @@ -35,4 +35,13 @@ groups: nodes, cluster_hosts, schedulable_nodes openshift_node_labels: role: app - with_items: "{{ groups['tag_openshift-role_app'] }}" + with_items: "{{ groups['tag_openshift_role_app'] }}" + +- name: Add app instances to host group + add_host: + name: "{{ hostvars[item].ec2_tag_Name }}" + groups: new_nodes + openshift_node_labels: + role: "{{ node_type }}" + with_items: "{{ groups['tag_provision_node'] }}" + when: add_node is defined diff --git a/reference-architecture/aws-ansible/playbooks/roles/rhsm-repos/tasks/main.yaml b/reference-architecture/aws-ansible/playbooks/roles/rhsm-repos/tasks/main.yaml index c5c1fde81..2053334f3 100644 --- a/reference-architecture/aws-ansible/playbooks/roles/rhsm-repos/tasks/main.yaml +++ b/reference-architecture/aws-ansible/playbooks/roles/rhsm-repos/tasks/main.yaml @@ -1,7 +1,6 @@ --- - name: disable unneeded repos command: subscription-manager repos --disable='*' - when: register_result | changed - name: ensure proper repos are assigned command: subscription-manager repos --enable={{ item }} @@ -9,3 +8,12 @@ - rhel-7-server-rpms - rhel-7-server-extras-rpms - rhel-7-server-ose-3.3-rpms + +- name: remove rhui repos + replace: + dest: "{{ item }}" + regexp: 'enabled=1' + replace: 'enabled=0' + with_items: + - /etc/yum.repos.d/redhat-rhui-client-config.repo + - /etc/yum.repos.d/redhat-rhui.repo diff --git a/reference-architecture/aws-ansible/playbooks/roles/rhsm-subscription/tasks/main.yaml b/reference-architecture/aws-ansible/playbooks/roles/rhsm-subscription/tasks/main.yaml index 9fbe9772e..c22bcc7fe 100644 --- a/reference-architecture/aws-ansible/playbooks/roles/rhsm-subscription/tasks/main.yaml +++ b/reference-architecture/aws-ansible/playbooks/roles/rhsm-subscription/tasks/main.yaml @@ -5,5 +5,4 @@ password: "{{ rhsm_password }}" state: present pool: "^{{ rhsm_pool }}" - register: register_result when: ansible_os_family == "RedHat"