diff options
author | Thomas Wiest <twiest@gmail.com> | 2014-10-23 15:35:16 -0400 |
---|---|---|
committer | Thomas Wiest <twiest@gmail.com> | 2014-10-23 15:35:16 -0400 |
commit | cde5cba28b7b0e9386f1c549ecf9141bbcadc64e (patch) | |
tree | 22a39c5589aa3f6a2a01f185041258e4fd69dea6 | |
parent | 1057c69acdaf47e2bcd4b395069e3bc1bd9eec88 (diff) | |
parent | 5f9c7eb2d2ad44776d33197857dcd0afe693b5f5 (diff) | |
download | openshift-cde5cba28b7b0e9386f1c549ecf9141bbcadc64e.tar.gz openshift-cde5cba28b7b0e9386f1c549ecf9141bbcadc64e.tar.bz2 openshift-cde5cba28b7b0e9386f1c549ecf9141bbcadc64e.tar.xz openshift-cde5cba28b7b0e9386f1c549ecf9141bbcadc64e.zip |
Merge pull request #11 from twiest/pull
Added atomic aws host to cloud.rb
48 files changed, 1749 insertions, 13 deletions
@@ -2,6 +2,7 @@ require 'thor' require_relative 'lib/gce_command' +require_relative 'lib/aws_command' # Don't buffer output to the client STDOUT.sync = true @@ -12,6 +13,9 @@ module OpenShift class CloudCommand < Thor desc 'gce', 'Manages Google Compute Engine assets' subcommand "gce", GceCommand + + desc 'aws', 'Manages Amazon Web Services assets' + subcommand "aws", AwsCommand end end end diff --git a/inventory/aws/ec2.ini b/inventory/aws/ec2.ini new file mode 100644 index 000000000..c6693bb1c --- /dev/null +++ b/inventory/aws/ec2.ini @@ -0,0 +1,56 @@ +# Ansible EC2 external inventory script settings +# + +[ec2] + +# to talk to a private eucalyptus instance uncomment these lines +# and edit edit eucalyptus_host to be the host name of your cloud controller +#eucalyptus = True +#eucalyptus_host = clc.cloud.domain.org + +# AWS regions to make calls to. Set this to 'all' to make request to all regions +# in AWS and merge the results together. Alternatively, set this to a comma +# separated list of regions. E.g. 'us-east-1,us-west-1,us-west-2' +#regions = all +regions = us-east-1 +regions_exclude = us-gov-west-1,cn-north-1 + +# When generating inventory, Ansible needs to know how to address a server. +# Each EC2 instance has a lot of variables associated with it. Here is the list: +# http://docs.pythonboto.org/en/latest/ref/ec2.html#module-boto.ec2.instance +# Below are 2 variables that are used as the address of a server: +# - destination_variable +# - vpc_destination_variable + +# This is the normal destination variable to use. If you are running Ansible +# from outside EC2, then 'public_dns_name' makes the most sense. If you are +# running Ansible from within EC2, then perhaps you want to use the internal +# address, and should set this to 'private_dns_name'. +destination_variable = public_dns_name + +# For server inside a VPC, using DNS names may not make sense. When an instance +# has 'subnet_id' set, this variable is used. If the subnet is public, setting +# this to 'ip_address' will return the public IP address. For instances in a +# private subnet, this should be set to 'private_ip_address', and Ansible must +# be run from with EC2. +vpc_destination_variable = ip_address + +# To tag instances on EC2 with the resource records that point to them from +# Route53, uncomment and set 'route53' to True. +route53 = False + +# Additionally, you can specify the list of zones to exclude looking up in +# 'route53_excluded_zones' as a comma-separated list. +# route53_excluded_zones = samplezone1.com, samplezone2.com + +# API calls to EC2 are slow. For this reason, we cache the results of an API +# call. Set this to the path you want cache files to be written to. Two files +# will be written to this directory: +# - ansible-ec2.cache +# - ansible-ec2.index +cache_path = ~/.ansible/tmp + +# The number of seconds a cache file is considered valid. After this many +# seconds, a new API call will be made, and the cache file will be updated. +# To disable the cache, set this value to 0 +cache_max_age = 300 diff --git a/inventory/aws/ec2.py b/inventory/aws/ec2.py new file mode 100755 index 000000000..84841d3f0 --- /dev/null +++ b/inventory/aws/ec2.py @@ -0,0 +1,610 @@ +#!/usr/bin/env python + +''' +EC2 external inventory script +================================= + +Generates inventory that Ansible can understand by making API request to +AWS EC2 using the Boto library. + +NOTE: This script assumes Ansible is being executed where the environment +variables needed for Boto have already been set: + export AWS_ACCESS_KEY_ID='AK123' + export AWS_SECRET_ACCESS_KEY='abc123' + +This script also assumes there is an ec2.ini file alongside it. To specify a +different path to ec2.ini, define the EC2_INI_PATH environment variable: + + export EC2_INI_PATH=/path/to/my_ec2.ini + +If you're using eucalyptus you need to set the above variables and +you need to define: + + export EC2_URL=http://hostname_of_your_cc:port/services/Eucalyptus + +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: + - ec2_ami_launch_index + - ec2_architecture + - ec2_association + - ec2_attachTime + - ec2_attachment + - ec2_attachmentId + - ec2_client_token + - ec2_deleteOnTermination + - ec2_description + - ec2_deviceIndex + - ec2_dns_name + - ec2_eventsSet + - ec2_group_name + - ec2_hypervisor + - ec2_id + - ec2_image_id + - ec2_instanceState + - ec2_instance_type + - ec2_ipOwnerId + - ec2_ip_address + - ec2_item + - ec2_kernel + - ec2_key_name + - ec2_launch_time + - ec2_monitored + - ec2_monitoring + - ec2_networkInterfaceId + - ec2_ownerId + - ec2_persistent + - ec2_placement + - ec2_platform + - ec2_previous_state + - ec2_private_dns_name + - ec2_private_ip_address + - ec2_publicIp + - ec2_public_dns_name + - ec2_ramdisk + - ec2_reason + - ec2_region + - ec2_requester_id + - ec2_root_device_name + - ec2_root_device_type + - ec2_security_group_ids + - ec2_security_group_names + - ec2_shutdown_state + - ec2_sourceDestCheck + - ec2_spot_instance_request_id + - ec2_state + - ec2_state_code + - ec2_state_reason + - ec2_status + - ec2_subnet_id + - ec2_tenancy + - ec2_virtualization_type + - ec2_vpc_id + +These variables are pulled out of a boto.ec2.instance object. There is a lack of +consistency with variable spellings (camelCase and underscores) since this +just loops through all variables the object exposes. It is preferred to use the +ones with underscores when multiple exist. + +In addition, if an instance has AWS Tags associated with it, each tag is a new +variable named: + - ec2_tag_[Key] = [Value] + +Security groups are comma-separated in 'ec2_security_group_ids' and +'ec2_security_group_names'. +''' + +# (c) 2012, Peter Sankauskas +# +# 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/>. + +###################################################################### + +import sys +import os +import argparse +import re +from time import time +import boto +from boto import ec2 +from boto import rds +from boto import route53 +import ConfigParser + +try: + import json +except ImportError: + import simplejson as json + + +class Ec2Inventory(object): + def _empty_inventory(self): + return {"_meta" : {"hostvars" : {}}} + + def __init__(self): + ''' Main execution path ''' + + # Inventory grouped by instance IDs, tags, security groups, regions, + # and availability zones + self.inventory = self._empty_inventory() + + # Index of hostname (address) to instance ID + self.index = {} + + # Read settings and parse CLI arguments + self.read_settings() + self.parse_cli_args() + + # Cache + if self.args.refresh_cache: + self.do_api_calls_update_cache() + elif not self.is_cache_valid(): + self.do_api_calls_update_cache() + + # Data to print + if self.args.host: + data_to_print = self.get_host_info() + + elif self.args.list: + # Display list of instances for inventory + if self.inventory == self._empty_inventory(): + data_to_print = self.get_inventory_from_cache() + else: + data_to_print = self.json_format_dict(self.inventory, True) + + print data_to_print + + + def is_cache_valid(self): + ''' Determines if the cache files have expired, or if it is still valid ''' + + if os.path.isfile(self.cache_path_cache): + mod_time = os.path.getmtime(self.cache_path_cache) + current_time = time() + if (mod_time + self.cache_max_age) > current_time: + if os.path.isfile(self.cache_path_index): + return True + + return False + + + def read_settings(self): + ''' Reads the settings from the ec2.ini file ''' + + 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) + config.read(ec2_ini_path) + + # is eucalyptus? + self.eucalyptus_host = None + self.eucalyptus = False + if config.has_option('ec2', 'eucalyptus'): + self.eucalyptus = config.getboolean('ec2', 'eucalyptus') + if self.eucalyptus and config.has_option('ec2', 'eucalyptus_host'): + self.eucalyptus_host = config.get('ec2', 'eucalyptus_host') + + # Regions + self.regions = [] + configRegions = config.get('ec2', 'regions') + 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) + else: + for regionInfo in ec2.regions(): + if regionInfo.name not in configRegions_exclude: + self.regions.append(regionInfo.name) + else: + self.regions = configRegions.split(",") + + # Destination addresses + self.destination_variable = config.get('ec2', 'destination_variable') + self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable') + + # Route53 + self.route53_enabled = config.getboolean('ec2', 'route53') + self.route53_excluded_zones = [] + if config.has_option('ec2', 'route53_excluded_zones'): + self.route53_excluded_zones.extend( + config.get('ec2', 'route53_excluded_zones', '').split(',')) + + # Cache related + cache_dir = os.path.expanduser(config.get('ec2', 'cache_path')) + 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" + self.cache_max_age = config.getint('ec2', 'cache_max_age') + + + + def parse_cli_args(self): + ''' Command line argument processing ''' + + parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on EC2') + parser.add_argument('--list', action='store_true', default=True, + help='List instances (default: True)') + parser.add_argument('--host', action='store', + 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)') + self.args = parser.parse_args() + + + def do_api_calls_update_cache(self): + ''' Do API calls to each region, and save data in cache files ''' + + if self.route53_enabled: + self.get_route53_records() + + for region in self.regions: + self.get_instances_by_region(region) + self.get_rds_instances_by_region(region) + + self.write_to_cache(self.inventory, self.cache_path_cache) + self.write_to_cache(self.index, self.cache_path_index) + + + 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) + + reservations = conn.get_all_instances() + for reservation in reservations: + for instance in reservation.instances: + 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) + + 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) + if conn: + instances = conn.get_all_dbinstances() + for instance in instances: + self.add_rds_instance(instance, region) + except boto.exception.BotoServerError, e: + if not e.reason == "Forbidden": + print "Looks like AWS RDS is down: " + print e + sys.exit(1) + + 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' + 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) + + reservations = conn.get_all_instances([instance_id]) + for reservation in reservations: + for instance in reservation.instances: + return instance + + + def add_instance(self, instance, region): + ''' Adds an instance to the inventory and index, as long as it is + addressable ''' + + # Only want running instances + if instance.state != 'running': + return + + # Select the best destination address + if instance.subnet_id: + dest = getattr(instance, self.vpc_destination_variable) + else: + dest = getattr(instance, self.destination_variable) + + if not dest: + # Skip instances we cannot address (e.g. private VPC subnet) + return + + # Add to index + self.index[dest] = [region, instance.id] + + # Inventory: Group by instance ID (always a group of 1) + self.inventory[instance.id] = [dest] + + # Inventory: Group by region + self.push(self.inventory, region, dest) + + # Inventory: Group by availability zone + self.push(self.inventory, instance.placement, dest) + + # Inventory: Group by instance type + self.push(self.inventory, self.to_safe('type_' + instance.instance_type), dest) + + # Inventory: Group by key pair + if instance.key_name: + self.push(self.inventory, self.to_safe('key_' + instance.key_name), dest) + + # Inventory: Group by security group + try: + for group in instance.groups: + key = self.to_safe("security_group_" + group.name) + self.push(self.inventory, key, dest) + except AttributeError: + print 'Package boto seems a bit older.' + print 'Please upgrade boto >= 2.3.0.' + sys.exit(1) + + # Inventory: Group by tag keys + for k, v in instance.tags.iteritems(): + key = self.to_safe("tag_" + k + "=" + v) + self.push(self.inventory, key, dest) + + # Inventory: Group by Route53 domain names if enabled + if self.route53_enabled: + route53_names = self.get_instance_route53_names(instance) + for name in route53_names: + self.push(self.inventory, name, dest) + + # Global Tag: tag all EC2 instances + self.push(self.inventory, 'ec2', dest) + + self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance) + + + def add_rds_instance(self, instance, region): + ''' Adds an RDS instance to the inventory and index, as long as it is + addressable ''' + + # Only want available instances + if instance.status != 'available': + return + + # Select the best destination address + #if instance.subnet_id: + #dest = getattr(instance, self.vpc_destination_variable) + #else: + #dest = getattr(instance, self.destination_variable) + dest = instance.endpoint[0] + + if not dest: + # Skip instances we cannot address (e.g. private VPC subnet) + return + + # Add to index + self.index[dest] = [region, instance.id] + + # Inventory: Group by instance ID (always a group of 1) + self.inventory[instance.id] = [dest] + + # Inventory: Group by region + self.push(self.inventory, region, dest) + + # Inventory: Group by availability zone + self.push(self.inventory, instance.availability_zone, dest) + + # Inventory: Group by instance type + self.push(self.inventory, self.to_safe('type_' + instance.instance_class), dest) + + # Inventory: Group by security group + try: + if instance.security_group: + key = self.to_safe("security_group_" + instance.security_group.name) + self.push(self.inventory, key, dest) + except AttributeError: + print 'Package boto seems a bit older.' + print 'Please upgrade boto >= 2.3.0.' + sys.exit(1) + + # Inventory: Group by engine + self.push(self.inventory, self.to_safe("rds_" + instance.engine), dest) + + # Inventory: Group by parameter group + self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), dest) + + # Global Tag: all RDS instances + self.push(self.inventory, 'rds', dest) + + + def get_route53_records(self): + ''' Get and store the map of resource records to domain names that + point to them. ''' + + r53_conn = route53.Route53Connection() + all_zones = r53_conn.get_zones() + + route53_zones = [ zone for zone in all_zones if zone.name[:-1] + not in self.route53_excluded_zones ] + + self.route53_records = {} + + for zone in route53_zones: + rrsets = r53_conn.get_all_rrsets(zone.id) + + for record_set in rrsets: + record_name = record_set.name + + if record_name.endswith('.'): + record_name = record_name[:-1] + + for resource in record_set.resource_records: + self.route53_records.setdefault(resource, set()) + self.route53_records[resource].add(record_name) + + + def get_instance_route53_names(self, instance): + ''' Check if an instance is referenced in the records we have from + Route53. If it is, return the list of domain names pointing to said + instance. If nothing points to it, return an empty list. ''' + + instance_attributes = [ 'public_dns_name', 'private_dns_name', + 'ip_address', 'private_ip_address' ] + + name_list = set() + + for attrib in instance_attributes: + try: + value = getattr(instance, attrib) + except AttributeError: + continue + + if value in self.route53_records: + name_list.update(self.route53_records[value]) + + return list(name_list) + + + def get_host_info_dict_from_instance(self, instance): + instance_vars = {} + for key in vars(instance): + value = getattr(instance, key) + key = self.to_safe('ec2_' + key) + + # Handle complex types + # state/previous_state changed to properties in boto in https://github.com/boto/boto/commit/a23c379837f698212252720d2af8dec0325c9518 + if key == 'ec2__state': + instance_vars['ec2_state'] = instance.state or '' + instance_vars['ec2_state_code'] = instance.state_code + elif key == 'ec2__previous_state': + instance_vars['ec2_previous_state'] = instance.previous_state or '' + 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]: + instance_vars[key] = value.strip() + elif type(value) == type(None): + instance_vars[key] = '' + elif key == 'ec2_region': + instance_vars[key] = value.name + elif key == 'ec2__placement': + instance_vars['ec2_placement'] = value.zone + elif key == 'ec2_tags': + for k, v in value.iteritems(): + key = self.to_safe('ec2_tag_' + k) + instance_vars[key] = v + elif key == 'ec2_groups': + group_ids = [] + group_names = [] + for group in value: + group_ids.append(group.id) + group_names.append(group.name) + instance_vars["ec2_security_group_ids"] = ','.join(group_ids) + instance_vars["ec2_security_group_names"] = ','.join(group_names) + else: + pass + # TODO Product codes if someone finds them useful + #print key + #print type(value) + #print value + + return instance_vars + + def get_host_info(self): + ''' Get variables about a specific host ''' + + if len(self.index) == 0: + # Need to load index from cache + self.load_index_from_cache() + + if not self.args.host in self.index: + # try updating the cache + self.do_api_calls_update_cache() + if not self.args.host in self.index: + # host migh not exist anymore + return self.json_format_dict({}, True) + + (region, instance_id) = self.index[self.args.host] + + instance = self.get_instance(region, instance_id) + return self.json_format_dict(self.get_host_info_dict_from_instance(instance), True) + + def push(self, my_dict, key, element): + ''' Pushed an element onto an array that may not have been defined in + the dict ''' + + if key in my_dict: + my_dict[key].append(element); + else: + my_dict[key] = [element] + + + def get_inventory_from_cache(self): + ''' Reads the inventory from the cache file and returns it as a JSON + object ''' + + cache = open(self.cache_path_cache, 'r') + json_inventory = cache.read() + return json_inventory + + + def load_index_from_cache(self): + ''' Reads the index from the cache file sets self.index ''' + + cache = open(self.cache_path_index, 'r') + json_index = cache.read() + self.index = json.loads(json_index) + + + def write_to_cache(self, data, filename): + ''' Writes data in JSON format to a file ''' + + json_data = self.json_format_dict(data, True) + cache = open(filename, 'w') + cache.write(json_data) + cache.close() + + + 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) + + + def json_format_dict(self, data, pretty=False): + ''' Converts a dict to a JSON object and dumps it as a formatted + string ''' + + if pretty: + return json.dumps(data, sort_keys=True, indent=2) + else: + return json.dumps(data) + + +# Run the script +Ec2Inventory() + diff --git a/lib/ansible_helper.rb b/lib/ansible_helper.rb index 876c16a44..76af73b0d 100755..100644 --- a/lib/ansible_helper.rb +++ b/lib/ansible_helper.rb @@ -60,7 +60,7 @@ extra_vars: #{@extra_vars.to_json} end def self.for_gce - ah = AnsibleHelper.new + ah = AnsibleHelper.new # GCE specific configs gce_ini = "#{MYDIR}/../inventory/gce/gce.ini" @@ -85,6 +85,14 @@ extra_vars: #{@extra_vars.to_json} return ah end + def self.for_aws + ah = AnsibleHelper.new + + ah.inventory = 'inventory/aws/ec2.py' + return ah + end + + def ignore_bug_6407 puts puts %q[ .---- Spurious warning "It is unnecessary to use '{{' in loops" (ansible bug 6407) ----.] diff --git a/lib/aws_command.rb b/lib/aws_command.rb new file mode 100644 index 000000000..d471557b8 --- /dev/null +++ b/lib/aws_command.rb @@ -0,0 +1,144 @@ +require 'thor' + +require_relative 'aws_helper' +require_relative 'launch_helper' + +module OpenShift + module Ops + class AwsCommand < Thor + # WARNING: we do not currently support environments with hyphens in the name + SUPPORTED_ENVS = %w(prod stg int tint kint test jint) + + option :type, :required => true, :enum => LaunchHelper.get_aws_host_types, + :desc => 'The host type of the new instances.' + option :env, :required => true, :aliases => '-e', :enum => SUPPORTED_ENVS, + :desc => 'The environment of the new instances.' + option :count, :default => 1, :aliases => '-c', :type => :numeric, + :desc => 'The number of instances to create' + option :tag, :type => :array, + :desc => 'The tag(s) to add to the new instances. Allowed characters are letters, numbers, and hyphens.' + desc "launch", "Launches instances." + def launch() + AwsHelper.check_creds() + + # Expand all of the instance names so that we have a complete array + names = [] + options[:count].times { names << "#{options[:env]}-#{options[:type]}-#{SecureRandom.hex(5)}" } + + ah = AnsibleHelper.for_aws() + + # AWS specific configs + ah.extra_vars['oo_new_inst_names'] = names + ah.extra_vars['oo_new_inst_tags'] = options[:tag] + ah.extra_vars['oo_env'] = options[:env] + + # Add a created by tag + ah.extra_vars['oo_new_inst_tags'] = {} if ah.extra_vars['oo_new_inst_tags'].nil? + + ah.extra_vars['oo_new_inst_tags']["created-by"] = ENV['USER'] + ah.extra_vars['oo_new_inst_tags'].merge!(AwsHelper.generate_env_tag(options[:env])) + ah.extra_vars['oo_new_inst_tags'].merge!(AwsHelper.generate_host_type_tag(options[:type])) + ah.extra_vars['oo_new_inst_tags'].merge!(AwsHelper.generate_env_host_type_tag(options[:env], options[:type])) + + puts + puts 'Creating instance(s) in AWS...' + ah.ignore_bug_6407 + + # Make sure we're completely up to date before launching + clear_cache() + ah.run_playbook("playbooks/aws/#{options[:type]}/launch.yml") + ensure + # This is so that if we a config right after a launch, the newly launched instances will be + # in the list. + clear_cache() + end + + desc "clear-cache", 'Clear the inventory cache' + def clear_cache() + print "Clearing inventory cache... " + AwsHelper.clear_inventory_cache() + puts "Done." + end + + option :name, :required => false, :type => :string, + :desc => 'The name of the instance to configure.' + option :env, :required => false, :aliases => '-e', :enum => SUPPORTED_ENVS, + :desc => 'The environment of the new instances.' + option :type, :required => false, :enum => LaunchHelper.get_aws_host_types, + :desc => 'The type of the instances to configure.' + desc "config", 'Configures instances.' + def config() + ah = AnsibleHelper.for_aws() + + abort 'Error: you can\'t specify both --name and --type' unless options[:type].nil? || options[:name].nil? + + abort 'Error: you can\'t specify both --name and --env' unless options[:env].nil? || options[:name].nil? + + host_type = nil + if options[:name] + details = AwsHelper.get_host_details(options[:name]) + ah.extra_vars['oo_host_group_exp'] = options[:name] + ah.extra_vars['oo_env'] = details['env'] + host_type = details['host-type'] + elsif options[:type] && options[:env] + oo_env_host_type_tag = AwsHelper.generate_env_host_type_tag_name(options[:env], options[:type]) + ah.extra_vars['oo_host_group_exp'] = "groups['#{oo_env_host_type_tag}']" + ah.extra_vars['oo_env'] = options[:env] + host_type = options[:type] + else + abort 'Error: you need to specify either --name or (--type and --env)' + end + + puts + puts "Configuring #{options[:type]} instance(s) in AWS..." + ah.ignore_bug_6407 + + ah.run_playbook("playbooks/aws/#{host_type}/config.yml") + end + + desc "list", "Lists instances." + def list() + AwsHelper.check_creds() + hosts = AwsHelper.get_hosts() + + puts + puts "Instances" + puts "---------" + hosts.each { |h| puts " #{h.name}.#{h.env}" } + puts + end + + desc "ssh", "Ssh to an instance" + def ssh(*ssh_ops, host) + if host =~ /^([\w\d_.-]+)@([\w\d-_.]+)/ + user = $1 + host = $2 + end + + details = AwsHelper.get_host_details(host) + abort "\nError: Instance [#{host}] is not RUNNING\n\n" unless details['ec2_state'] == 'running' + + cmd = "ssh #{ssh_ops.join(' ')}" + + if user.nil? + cmd += " " + else + cmd += " #{user}@" + end + + cmd += "#{details['ec2_ip_address']}" + + exec(cmd) + end + + desc 'types', 'Displays instance types' + def types() + puts + puts "Available Host Types" + puts "--------------------" + LaunchHelper.get_aws_host_types.each { |t| puts " #{t}" } + puts + end + end + end +end diff --git a/lib/aws_helper.rb b/lib/aws_helper.rb new file mode 100644 index 000000000..6d213107b --- /dev/null +++ b/lib/aws_helper.rb @@ -0,0 +1,82 @@ +require 'fileutils' + +module OpenShift + module Ops + class AwsHelper + MYDIR = File.expand_path(File.dirname(__FILE__)) + + def self.get_list() + cmd = "#{MYDIR}/../inventory/aws/ec2.py --list" + hosts = %x[#{cmd} 2>&1] + + raise "Error: failed to list hosts\n#{hosts}" unless $?.exitstatus == 0 + return JSON.parse(hosts) + end + + def self.get_hosts() + hosts = get_list() + + retval = [] + hosts['_meta']['hostvars'].each do |host, info| + retval << OpenStruct.new({ + :name => info['ec2_tag_Name'], + :env => info['ec2_tag_environment'] || 'UNSET', + :external_ip => info['ec2_ip_address'], + :public_dns => info['ec2_public_dns_name'] + }) + end + + retval.sort_by! { |h| [h.env, h.name] } + + return retval + end + + def self.get_host_details(host) + hosts = get_list() + dns_names = hosts["tag_Name_#{host}"] + + raise "Error: host not found [#{host}]" if dns_names.nil? + + return hosts['_meta']['hostvars'][dns_names.first] + end + + def self.check_creds() + raise "AWS_ACCESS_KEY_ID environment variable must be set" if ENV['AWS_ACCESS_KEY_ID'].nil? + raise "AWS_SECRET_ACCESS_KEY environment variable must be set" if ENV['AWS_SECRET_ACCESS_KEY'].nil? + end + + def self.clear_inventory_cache() + path = "#{ENV['HOME']}/.ansible/tmp" + cache_files = ["#{path}/ansible-ec2.cache", "#{path}/ansible-ec2.index"] + FileUtils.rm(cache_files) + end + + def self.generate_env_tag(env) + return { "environment" => env } + end + + def self.generate_env_tag_name(env) + h = generate_env_tag(env) + return "tag_#{h.keys.first}_#{h.values.first}" + end + + def self.generate_host_type_tag(host_type) + return { "host-type" => host_type } + end + + def self.generate_host_type_tag_name(host_type) + h = generate_host_type_tag(host_type) + return "tag_#{h.keys.first}_#{h.values.first}" + end + + def self.generate_env_host_type_tag(env, host_type) + return { "env-host-type" => "#{env}-#{host_type}" } + end + + def self.generate_env_host_type_tag_name(env, host_type) + h = generate_env_host_type_tag(env, host_type) + return "tag_#{h.keys.first}_#{h.values.first}" + end + end + end +end diff --git a/lib/gce_command.rb b/lib/gce_command.rb index 6a6b46228..ce3737a19 100755..100644 --- a/lib/gce_command.rb +++ b/lib/gce_command.rb @@ -125,17 +125,12 @@ module OpenShift desc "list", "Lists instances." def list() - hosts = GceHelper.list_hosts() - - data = {} - hosts.each do |key,value| - value.each { |h| (data[h] ||= []) << key } - end + hosts = GceHelper.get_hosts() puts puts "Instances" puts "---------" - data.keys.sort.each { |k| puts " #{k}" } + hosts.each { |k| puts " #{k.name}" } puts end @@ -177,13 +172,10 @@ module OpenShift desc "ssh", "Ssh to an instance" def ssh(*ssh_ops, host) - puts host if host =~ /^([\w\d_.-]+)@([\w\d-_.]+)/ user = $1 host = $2 end - puts "user=#{user}" - puts "host=#{host}" details = GceHelper.get_host_details(host) abort "\nError: Instance [#{host}] is not RUNNING\n\n" unless details['gce_status'] == 'RUNNING' diff --git a/lib/gce_helper.rb b/lib/gce_helper.rb index 6c0f57cf3..2ff716ce1 100755..100644 --- a/lib/gce_helper.rb +++ b/lib/gce_helper.rb @@ -1,15 +1,27 @@ +require 'ostruct' + module OpenShift module Ops class GceHelper MYDIR = File.expand_path(File.dirname(__FILE__)) - def self.list_hosts() + def self.get_hosts() cmd = "#{MYDIR}/../inventory/gce/gce.py --list" hosts = %x[#{cmd} 2>&1] raise "Error: failed to list hosts\n#{hosts}" unless $?.exitstatus == 0 - return JSON.parse(hosts) + # invert the hash so that it's key is the host, and values is an array of metadata + data = {} + JSON.parse(hosts).each do |key,value| + value.each { |h| (data[h] ||= []) << key } + end + + # For now, we only care about the name. In the future, we may want the other metadata included. + retval = [] + data.keys.sort.each { |k| retval << OpenStruct.new({ :name => k }) } + + return retval end def self.get_host_details(host) diff --git a/lib/launch_helper.rb b/lib/launch_helper.rb index 2033f3ddb..0fe5ea6dc 100755..100644 --- a/lib/launch_helper.rb +++ b/lib/launch_helper.rb @@ -21,6 +21,10 @@ module OpenShift def self.get_gce_host_types() return Dir.glob("#{MYDIR}/../playbooks/gce/*").map { |d| File.basename(d) } end + + def self.get_aws_host_types() + return Dir.glob("#{MYDIR}/../playbooks/aws/*").map { |d| File.basename(d) } + end end end end diff --git a/playbooks/aws/os2-atomic-proxy/config.yml b/playbooks/aws/os2-atomic-proxy/config.yml new file mode 100644 index 000000000..7d719a121 --- /dev/null +++ b/playbooks/aws/os2-atomic-proxy/config.yml @@ -0,0 +1,21 @@ +--- +- name: "populate oo_hosts_to_config host group if needed" + hosts: localhost + gather_facts: no + tasks: + - name: Evaluate oo_host_group_exp if it's set + add_host: "name={{ item }} groups=oo_hosts_to_config" + with_items: "{{ oo_host_group_exp | default(['']) }}" + when: oo_host_group_exp is defined + +- name: "Configure instances" + hosts: oo_hosts_to_config + connection: ssh + user: root + vars_files: + - vars.yml + - "vars.{{ oo_env }}.yml" + roles: + - ../../../roles/atomic_base + - ../../../roles/atomic_proxy + - ../../../roles/shutdown_nightly diff --git a/playbooks/aws/os2-atomic-proxy/launch.yml b/playbooks/aws/os2-atomic-proxy/launch.yml new file mode 100644 index 000000000..23bf67bb7 --- /dev/null +++ b/playbooks/aws/os2-atomic-proxy/launch.yml @@ -0,0 +1,69 @@ +--- +- name: Launch instance(s) + hosts: localhost + connection: local + gather_facts: no + + vars: + inst_region: us-east-1 + atomic_ami: ami-8e239fe6 + user_data_file: user_data.txt + + vars_files: + - vars.yml + + tasks: + - name: Launch instances + ec2: + state: present + region: "{{ inst_region }}" + keypair: mmcgrath_libra + group: ['Libra', '{{ oo_env }}', '{{ oo_env }}_proxy', '{{ oo_env }}_proxy_atomic'] + instance_type: m3.large + image: "{{ atomic_ami }}" + count: "{{ oo_new_inst_names | oo_len }}" + user_data: "{{ lookup('file', user_data_file) }}" + wait: yes + register: ec2 + + - name: Add new instances public IPs to the atomic proxy host group + add_host: "hostname={{ item.public_ip }} groupname=new_ec2_instances" + with_items: ec2.instances + + - name: Add Name and environment tags to instances + ec2_tag: "resource={{ item.1.id }} region={{ inst_region }} state=present" + with_together: + - oo_new_inst_names + - ec2.instances + args: + tags: + Name: "{{ item.0 }}" + + - name: Add other tags to instances + ec2_tag: "resource={{ item.id }} region={{ inst_region }} state=present" + with_items: ec2.instances + args: + tags: "{{ oo_new_inst_tags }}" + + - name: Add new instances public IPs to oo_hosts_to_config + add_host: "hostname={{ item.0 }} ansible_ssh_host={{ item.1.public_ip }} groupname=oo_hosts_to_config" + with_together: + - oo_new_inst_names + - ec2.instances + + - debug: var=ec2 + + - name: Wait for ssh + wait_for: "port=22 host={{ item.public_ip }}" + with_items: ec2.instances + + - name: Wait for root user setup + command: "ssh -o StrictHostKeyChecking=no -o PasswordAuthentication=no -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null root@{{ item.public_ip }} echo root user is setup" + register: result + until: result.rc == 0 + retries: 20 + delay: 10 + with_items: ec2.instances + +# Apply the configs, seprate so that just the configs can be run by themselves +- include: config.yml diff --git a/playbooks/aws/os2-atomic-proxy/user_data.txt b/playbooks/aws/os2-atomic-proxy/user_data.txt new file mode 100644 index 000000000..643d17c32 --- /dev/null +++ b/playbooks/aws/os2-atomic-proxy/user_data.txt @@ -0,0 +1,6 @@ +#cloud-config +disable_root: 0 + +system_info: + default_user: + name: root diff --git a/playbooks/aws/os2-atomic-proxy/vars.stg.yml b/playbooks/aws/os2-atomic-proxy/vars.stg.yml new file mode 100644 index 000000000..fa37b7ee3 --- /dev/null +++ b/playbooks/aws/os2-atomic-proxy/vars.stg.yml @@ -0,0 +1,2 @@ +--- +oo_env_long: staging diff --git a/playbooks/aws/os2-atomic-proxy/vars.yml b/playbooks/aws/os2-atomic-proxy/vars.yml new file mode 100644 index 000000000..ed97d539c --- /dev/null +++ b/playbooks/aws/os2-atomic-proxy/vars.yml @@ -0,0 +1 @@ +--- diff --git a/roles/atomic_base/README.md b/roles/atomic_base/README.md new file mode 100644 index 000000000..8fe3faf7d --- /dev/null +++ b/roles/atomic_base/README.md @@ -0,0 +1,56 @@ +Role Name +======== + +The purpose of this role is to do common configurations for all RHEL atomic hosts. + + +Requirements +------------ + +None + + +Role Variables +-------------- + +None + + +Dependencies +------------ + +None + + +Example Playbook +------------------------- + +From a group playbook: + + hosts: servers + roles: + - ../../roles/atomic_base + + +License +------- + +Copyright 2012-2014 Red Hat, Inc., All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +Author Information +------------------ + +Thomas Wiest <twiest@redhat.com> diff --git a/roles/atomic_base/defaults/main.yml b/roles/atomic_base/defaults/main.yml new file mode 100644 index 000000000..09eac6567 --- /dev/null +++ b/roles/atomic_base/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for atomic_base diff --git a/roles/atomic_base/files/bash/bashrc b/roles/atomic_base/files/bash/bashrc new file mode 100644 index 000000000..446f18f22 --- /dev/null +++ b/roles/atomic_base/files/bash/bashrc @@ -0,0 +1,12 @@ +# .bashrc + +# User specific aliases and functions + +alias rm='rm -i' +alias cp='cp -i' +alias mv='mv -i' + +# Source global definitions +if [ -f /etc/bashrc ]; then + . /etc/bashrc +fi diff --git a/roles/atomic_base/files/ostree/repo_config b/roles/atomic_base/files/ostree/repo_config new file mode 100644 index 000000000..7038158f9 --- /dev/null +++ b/roles/atomic_base/files/ostree/repo_config @@ -0,0 +1,10 @@ +[core] +repo_version=1 +mode=bare + +[remote "rh-atomic-controller"] +url=https://mirror.openshift.com/libra/ostree/rhel-7-atomic-host +branches=rh-atomic-controller/el7/x86_64/buildmaster/controller/docker; +tls-client-cert-path=/var/lib/yum/client-cert.pem +tls-client-key-path=/var/lib/yum/client-key.pem +gpg-verify=false diff --git a/roles/atomic_base/files/system/90-nofile.conf b/roles/atomic_base/files/system/90-nofile.conf new file mode 100644 index 000000000..8537a4c5f --- /dev/null +++ b/roles/atomic_base/files/system/90-nofile.conf @@ -0,0 +1,7 @@ +# PAM process file descriptor limits +# see limits.conf(5) for details. +#Each line describes a limit for a user in the form: +# +#<domain> <type> <item> <value> +* hard nofile 16384 +root soft nofile 16384 diff --git a/roles/atomic_base/handlers/main.yml b/roles/atomic_base/handlers/main.yml new file mode 100644 index 000000000..a9481f6c7 --- /dev/null +++ b/roles/atomic_base/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for atomic_base diff --git a/roles/atomic_base/meta/main.yml b/roles/atomic_base/meta/main.yml new file mode 100644 index 000000000..9578ab809 --- /dev/null +++ b/roles/atomic_base/meta/main.yml @@ -0,0 +1,19 @@ +--- +galaxy_info: + author: Thomas Wiest + description: Common base RHEL atomic configurations + company: Red Hat + # Some suggested licenses: + # - BSD (default) + # - MIT + # - GPLv2 + # - GPLv3 + # - Apache + # - CC-BY + license: Apache + min_ansible_version: 1.2 + platforms: + - name: EL + versions: + - 7 +dependencies: [] diff --git a/roles/atomic_base/tasks/bash.yml b/roles/atomic_base/tasks/bash.yml new file mode 100644 index 000000000..6e577971a --- /dev/null +++ b/roles/atomic_base/tasks/bash.yml @@ -0,0 +1,13 @@ +--- +- name: Copy .bashrc + copy: src=bash/bashrc dest=/root/.bashrc owner=root group=root mode=0644 + +- name: Link to .profile to .bashrc + file: src=/root/.bashrc dest=/root/.profile owner=root group=root state=link + +- name: Setup Timezone [{{ oo_timezone }}] + file: > + src=/usr/share/zoneinfo/{{ oo_timezone }} + dest=/etc/localtime + owner=root + group=root state=link diff --git a/roles/atomic_base/tasks/cloud_user.yml b/roles/atomic_base/tasks/cloud_user.yml new file mode 100644 index 000000000..e7347fc3d --- /dev/null +++ b/roles/atomic_base/tasks/cloud_user.yml @@ -0,0 +1,6 @@ +--- +- name: Remove cloud-user account + user: name=cloud-user state=absent remove=yes force=yes + +- name: Remove cloud-user sudo + file: path=/etc/sudoers.d/90-cloud-init-users state=absent diff --git a/roles/atomic_base/tasks/main.yml b/roles/atomic_base/tasks/main.yml new file mode 100644 index 000000000..5d8e8571a --- /dev/null +++ b/roles/atomic_base/tasks/main.yml @@ -0,0 +1,4 @@ +--- +- include: system.yml +- include: bash.yml +- include: ostree.yml diff --git a/roles/atomic_base/tasks/ostree.yml b/roles/atomic_base/tasks/ostree.yml new file mode 100644 index 000000000..b9d366f1b --- /dev/null +++ b/roles/atomic_base/tasks/ostree.yml @@ -0,0 +1,18 @@ +--- +- name: Copy ostree repo config + copy: > + src=ostree/repo_config + dest=/ostree/repo/config + owner=root + group=root + mode=0644 + +- name: "WORK AROUND: Stat redhat repo file" + stat: path=/etc/yum.repos.d/redhat.repo + register: redhat_repo + +- name: "WORK AROUND: subscription manager failures" + file: > + path=/etc/yum.repos.d/redhat.repo + state=touch + when: redhat_repo.stat.exists == False diff --git a/roles/atomic_base/tasks/system.yml b/roles/atomic_base/tasks/system.yml new file mode 100644 index 000000000..e5cde427d --- /dev/null +++ b/roles/atomic_base/tasks/system.yml @@ -0,0 +1,3 @@ +--- +- name: Upload nofile limits.d file + copy: src=system/90-nofile.conf dest=/etc/security/limits.d/90-nofile.conf owner=root group=root mode=0644 diff --git a/roles/atomic_base/vars/main.yml b/roles/atomic_base/vars/main.yml new file mode 100644 index 000000000..d4e61175c --- /dev/null +++ b/roles/atomic_base/vars/main.yml @@ -0,0 +1,2 @@ +--- +oo_timezone: US/Eastern diff --git a/roles/atomic_proxy/README.md b/roles/atomic_proxy/README.md new file mode 100644 index 000000000..348eaee1f --- /dev/null +++ b/roles/atomic_proxy/README.md @@ -0,0 +1,56 @@ +Role Name +======== + +The purpose of this role is to do common configurations for all RHEL atomic hosts. + + +Requirements +------------ + +None + + +Role Variables +-------------- + +None + + +Dependencies +------------ + +None + + +Example Playbook +------------------------- + +From a group playbook: + + hosts: servers + roles: + - ../../roles/atomic_proxy + + +License +------- + +Copyright 2012-2014 Red Hat, Inc., All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +Author Information +------------------ + +Thomas Wiest <twiest@redhat.com> diff --git a/roles/atomic_proxy/defaults/main.yml b/roles/atomic_proxy/defaults/main.yml new file mode 100644 index 000000000..0da428c27 --- /dev/null +++ b/roles/atomic_proxy/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for atomic_proxy diff --git a/roles/atomic_proxy/files/ctr-proxy-1.service b/roles/atomic_proxy/files/ctr-proxy-1.service new file mode 100644 index 000000000..c532eb8e8 --- /dev/null +++ b/roles/atomic_proxy/files/ctr-proxy-1.service @@ -0,0 +1,37 @@ + + +[Unit] +Description=Container proxy-1 + + +[Service] +Type=simple +TimeoutStartSec=5m +Slice=container-small.slice + +ExecStartPre=-/usr/bin/docker rm "proxy-1" + +ExecStart=/usr/bin/docker run --rm --name "proxy-1" \ + --volumes-from proxy-shared-data-1 \ + -a stdout -a stderr -p 80:80 -p 443:443 -p 4999:4999 \ + "proxy:latest" + +ExecStartPost=-/usr/bin/gear init --post "proxy-1" "proxy:latest" +ExecReload=-/usr/bin/docker stop "proxy-1" +ExecReload=-/usr/bin/docker rm "proxy-1" +ExecStop=-/usr/bin/docker stop "proxy-1" + +[Install] +WantedBy=container.target + +# Container information +X-ContainerId=proxy-1 +X-ContainerImage=proxy:latest +X-ContainerUserId= +X-ContainerRequestId=LwiWtYWaAvSavH6Ze53QJg +X-ContainerType=simple +X-PortMapping=80:80 +X-PortMapping=443:443 +X-PortMapping=4999:4999 + + diff --git a/roles/atomic_proxy/files/ctr-proxy-monitoring-1.service b/roles/atomic_proxy/files/ctr-proxy-monitoring-1.service new file mode 100644 index 000000000..7a91ea02c --- /dev/null +++ b/roles/atomic_proxy/files/ctr-proxy-monitoring-1.service @@ -0,0 +1,37 @@ + + +[Unit] +Description=Container proxy-monitoring-1 + + +[Service] +Type=simple +TimeoutStartSec=5m +Slice=container-small.slice + +ExecStartPre=-/usr/bin/docker rm "proxy-monitoring-1" + +ExecStart=/usr/bin/docker run --rm --name "proxy-monitoring-1" \ + --volumes-from proxy-shared-data-1 \ + -a stdout -a stderr \ + "monitoring:latest" + +ExecStartPost=-/usr/bin/gear init --post "proxy-monitoring-1" "monitoring:latest" +ExecReload=-/usr/bin/docker stop "proxy-monitoring-1" +ExecReload=-/usr/bin/docker rm "proxy-monitoring-1" +ExecStop=-/usr/bin/docker stop "proxy-monitoring-1" + +[Install] +WantedBy=container.target + +# Container information +X-ContainerId=proxy-monitoring-1 +X-ContainerImage=monitoring:latest +X-ContainerUserId= +X-ContainerRequestId=LwiWtYWaAvSavH6Ze53QJg +X-ContainerType=simple +X-PortMapping=80:80 +X-PortMapping=443:443 +X-PortMapping=4999:4999 + + diff --git a/roles/atomic_proxy/files/ctr-proxy-puppet-1.service b/roles/atomic_proxy/files/ctr-proxy-puppet-1.service new file mode 100644 index 000000000..c1f4d9b13 --- /dev/null +++ b/roles/atomic_proxy/files/ctr-proxy-puppet-1.service @@ -0,0 +1,37 @@ + + +[Unit] +Description=Container proxy-puppet-1 + + +[Service] +Type=simple +TimeoutStartSec=5m +Slice=container-small.slice + + +ExecStartPre=-/usr/bin/docker rm "proxy-puppet-1" + +ExecStart=/usr/bin/docker run --rm --name "proxy-puppet-1" \ + --volumes-from proxy-shared-data-1 \ + -v /var/lib/docker/volumes/proxy_puppet/var/lib/puppet/ssl:/var/lib/puppet/ssl \ + -v /var/lib/docker/volumes/proxy_puppet/etc/puppet:/etc/puppet \ + -a stdout -a stderr \ + "puppet:latest" +# Set links (requires container have a name) +ExecStartPost=-/usr/bin/gear init --post "proxy-puppet-1" "puppet:latest" +ExecReload=-/usr/bin/docker stop "proxy-puppet-1" +ExecReload=-/usr/bin/docker rm "proxy-puppet-1" +ExecStop=-/usr/bin/docker stop "proxy-puppet-1" + +[Install] +WantedBy=container.target + +# Container information +X-ContainerId=proxy-puppet-1 +X-ContainerImage=puppet:latest +X-ContainerUserId= +X-ContainerRequestId=Ky0lhw0onwoSDJR4GK6t3g +X-ContainerType=simple + + diff --git a/roles/atomic_proxy/files/proxy_containers_deploy_descriptor.json b/roles/atomic_proxy/files/proxy_containers_deploy_descriptor.json new file mode 100644 index 000000000..c15835d48 --- /dev/null +++ b/roles/atomic_proxy/files/proxy_containers_deploy_descriptor.json @@ -0,0 +1,29 @@ +{ + "Containers":[ + { + "Name":"proxy-puppet", + "Count":1, + "Image":"puppet:latest", + "PublicPorts":[ + ] + }, + { + "Name":"proxy", + "Count":1, + "Image":"proxy:latest", + "PublicPorts":[ + {"Internal":80,"External":80}, + {"Internal":443,"External":443}, + {"Internal":4999,"External":4999} + ] + }, + { + "Name":"proxy-monitoring", + "Count":1, + "Image":"monitoring:latest", + "PublicPorts":[ + ] + } + ], + "RandomizeIds": false +} diff --git a/roles/atomic_proxy/files/puppet/auth.conf b/roles/atomic_proxy/files/puppet/auth.conf new file mode 100644 index 000000000..b31906bae --- /dev/null +++ b/roles/atomic_proxy/files/puppet/auth.conf @@ -0,0 +1,116 @@ +# This is the default auth.conf file, which implements the default rules +# used by the puppet master. (That is, the rules below will still apply +# even if this file is deleted.) +# +# The ACLs are evaluated in top-down order. More specific stanzas should +# be towards the top of the file and more general ones at the bottom; +# otherwise, the general rules may "steal" requests that should be +# governed by the specific rules. +# +# See http://docs.puppetlabs.com/guides/rest_auth_conf.html for a more complete +# description of auth.conf's behavior. +# +# Supported syntax: +# Each stanza in auth.conf starts with a path to match, followed +# by optional modifiers, and finally, a series of allow or deny +# directives. +# +# Example Stanza +# --------------------------------- +# path /path/to/resource # simple prefix match +# # path ~ regex # alternately, regex match +# [environment envlist] +# [method methodlist] +# [auth[enthicated] {yes|no|on|off|any}] +# allow [host|backreference|*|regex] +# deny [host|backreference|*|regex] +# allow_ip [ip|cidr|ip_wildcard|*] +# deny_ip [ip|cidr|ip_wildcard|*] +# +# The path match can either be a simple prefix match or a regular +# expression. `path /file` would match both `/file_metadata` and +# `/file_content`. Regex matches allow the use of backreferences +# in the allow/deny directives. +# +# The regex syntax is the same as for Ruby regex, and captures backreferences +# for use in the `allow` and `deny` lines of that stanza +# +# Examples: +# +# path ~ ^/path/to/resource # Equivalent to `path /path/to/resource`. +# allow * # Allow all authenticated nodes (since auth +# # defaults to `yes`). +# +# path ~ ^/catalog/([^/]+)$ # Permit nodes to access their own catalog (by +# allow $1 # certname), but not any other node's catalog. +# +# path ~ ^/file_(metadata|content)/extra_files/ # Only allow certain nodes to +# auth yes # access the "extra_files" +# allow /^(.+)\.example\.com$/ # mount point; note this must +# allow_ip 192.168.100.0/24 # go ABOVE the "/file" rule, +# # since it is more specific. +# +# environment:: restrict an ACL to a comma-separated list of environments +# method:: restrict an ACL to a comma-separated list of HTTP methods +# auth:: restrict an ACL to an authenticated or unauthenticated request +# the default when unspecified is to restrict the ACL to authenticated requests +# (ie exactly as if auth yes was present). +# + +### Authenticated ACLs - these rules apply only when the client +### has a valid certificate and is thus authenticated + +# allow nodes to retrieve their own catalog +path ~ ^/catalog/([^/]+)$ +method find +allow $1 + +# allow nodes to retrieve their own node definition +path ~ ^/node/([^/]+)$ +method find +allow $1 + +# allow all nodes to access the certificates services +path /certificate_revocation_list/ca +method find +allow * + +# allow all nodes to store their own reports +path ~ ^/report/([^/]+)$ +method save +allow $1 + +# Allow all nodes to access all file services; this is necessary for +# pluginsync, file serving from modules, and file serving from custom +# mount points (see fileserver.conf). Note that the `/file` prefix matches +# requests to both the file_metadata and file_content paths. See "Examples" +# above if you need more granular access control for custom mount points. +path /file +allow * + +### Unauthenticated ACLs, for clients without valid certificates; authenticated +### clients can also access these paths, though they rarely need to. + +# allow access to the CA certificate; unauthenticated nodes need this +# in order to validate the puppet master's certificate +path /certificate/ca +auth any +method find +allow * + +# allow nodes to retrieve the certificate they requested earlier +path /certificate/ +auth any +method find +allow * + +# allow nodes to request a new certificate +path /certificate_request +auth any +method find, save +allow * + +# deny everything else; this ACL is not strictly necessary, but +# illustrates the default policy. +path / +auth any diff --git a/roles/atomic_proxy/files/setup-proxy-containers.sh b/roles/atomic_proxy/files/setup-proxy-containers.sh new file mode 100755 index 000000000..d047c96c1 --- /dev/null +++ b/roles/atomic_proxy/files/setup-proxy-containers.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +function fail { + msg=$1 + echo + echo $msg + echo + exit 5 +} + + +NUM_DATA_CTR=$(docker ps -a | grep -c proxy-shared-data-1) +[ "$NUM_DATA_CTR" -ne 0 ] && fail "ERROR: proxy-shared-data-1 exists" + + +# pre-cache the container images +echo +timeout --signal TERM --kill-after 30 600 docker pull busybox:latest || fail "ERROR: docker pull of busybox failed" + +echo +# WORKAROUND: Setup the shared data container +/usr/bin/docker run --name "proxy-shared-data-1" \ + -v /shared/etc/haproxy \ + -v /shared/etc/httpd \ + -v /shared/etc/openshift \ + -v /shared/etc/pki \ + -v /shared/var/run/ctr-ipc \ + -v /shared/var/lib/haproxy \ + -v /shared/usr/local \ + "busybox:latest" true + +# WORKAROUND: These are because we're not using a pod yet +cp /usr/local/etc/ctr-proxy-1.service /usr/local/etc/ctr-proxy-puppet-1.service /usr/local/etc/ctr-proxy-monitoring-1.service /etc/systemd/system/ + +systemctl daemon-reload + +echo +echo -n "sleeping 10 seconds for systemd reload to take affect..." +sleep 10 +echo " Done." + +# Start the services +systemctl start ctr-proxy-puppet-1 ctr-proxy-1 ctr-proxy-monitoring-1 diff --git a/roles/atomic_proxy/handlers/main.yml b/roles/atomic_proxy/handlers/main.yml new file mode 100644 index 000000000..8de31258f --- /dev/null +++ b/roles/atomic_proxy/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for atomic_proxy diff --git a/roles/atomic_proxy/meta/main.yml b/roles/atomic_proxy/meta/main.yml new file mode 100644 index 000000000..a92d685b1 --- /dev/null +++ b/roles/atomic_proxy/meta/main.yml @@ -0,0 +1,21 @@ +--- +galaxy_info: + author: Thomas Wiest + description: Common base RHEL atomic configurations + company: Red Hat + # Some suggested licenses: + # - BSD (default) + # - MIT + # - GPLv2 + # - GPLv3 + # - Apache + # - CC-BY + license: Apache + min_ansible_version: 1.2 + platforms: + - name: EL + versions: + - 7 +dependencies: + # This is the role's PRIVATE counterpart, which is used. + - ../../../../../atomic_private/ansible/roles/atomic_proxy diff --git a/roles/atomic_proxy/tasks/main.yml b/roles/atomic_proxy/tasks/main.yml new file mode 100644 index 000000000..d5a5a0a47 --- /dev/null +++ b/roles/atomic_proxy/tasks/main.yml @@ -0,0 +1,21 @@ +--- +- name: upload sbin scripts + copy: > + src={{ item }} + dest=/usr/local/sbin/{{ item }} + mode=0750 + with_items: + - setup-proxy-containers.sh + +- name: upload /usr/local/etc files + copy: > + src={{ item }} + dest=/usr/local/etc/{{ item }} + mode=0640 + with_items: + - proxy_containers_deploy_descriptor.json + - ctr-proxy-1.service + - ctr-proxy-puppet-1.service + - ctr-proxy-monitoring-1.service + +- include: setup_puppet.yml diff --git a/roles/atomic_proxy/tasks/setup_puppet.yml b/roles/atomic_proxy/tasks/setup_puppet.yml new file mode 100644 index 000000000..e711d06c1 --- /dev/null +++ b/roles/atomic_proxy/tasks/setup_puppet.yml @@ -0,0 +1,24 @@ +--- +- name: make puppet conf dir + file: > + dest={{ oo_proxy_puppet_volume_dir }}/etc/puppet + mode=755 + owner=root + group=root + state=directory + +- name: upload puppet auth config + copy: > + src=puppet/auth.conf + dest={{ oo_proxy_puppet_volume_dir }}/etc/puppet/auth.conf + mode=0644 + owner=root + group=root + +- name: upload puppet config + template: > + src=puppet/puppet.conf.j2 + dest={{ oo_proxy_puppet_volume_dir }}/etc/puppet/puppet.conf + mode=0644 + owner=root + group=root diff --git a/roles/atomic_proxy/templates/puppet/puppet.conf.j2 b/roles/atomic_proxy/templates/puppet/puppet.conf.j2 new file mode 100644 index 000000000..9a47ab11c --- /dev/null +++ b/roles/atomic_proxy/templates/puppet/puppet.conf.j2 @@ -0,0 +1,40 @@ +[main] + # we need to override the host name of the container + certname = ctr-proxy.stg.rhcloud.com + + # The Puppet log directory. + # The default value is '$vardir/log'. + logdir = /var/log/puppet + + # Where Puppet PID files are kept. + # The default value is '$vardir/run'. + rundir = /var/run/puppet + + # Where SSL certificates are kept. + # The default value is '$confdir/ssl'. + ssldir = $vardir/ssl + manifest = $manifestdir/site.pp + manifestdir = /var/lib/puppet/environments/pub/$environment/manifests + environment = {{ oo_env_long }} + modulepath = /var/lib/puppet/environments/pub/$environment/modules:/var/lib/puppet/environments/pri/$environment/modules:/var/lib/puppet/environments/pri/production/modules:$confdir/modules:/usr/share/puppet/modules + +[agent] + # The file in which puppetd stores a list of the classes + # associated with the retrieved configuratiion. Can be loaded in + # the separate ``puppet`` executable using the ``--loadclasses`` + # option. + # The default value is '$confdir/classes.txt'. + classfile = $vardir/classes.txt + + # Where puppetd caches the local configuration. An + # extension indicating the cache format is added automatically. + # The default value is '$confdir/localconfig'. + localconfig = $vardir/localconfig + server = puppet.ops.rhcloud.com + environment = {{ oo_env_long }} + pluginsync = true + graph = true + configtimeout = 600 + report = true + runinterval = 3600 + splay = true diff --git a/roles/atomic_proxy/templates/sync/sync-proxy-configs.sh.j2 b/roles/atomic_proxy/templates/sync/sync-proxy-configs.sh.j2 new file mode 100755 index 000000000..d9aa2d811 --- /dev/null +++ b/roles/atomic_proxy/templates/sync/sync-proxy-configs.sh.j2 @@ -0,0 +1,16 @@ +#!/bin/bash + +VOL_DIR=/var/lib/docker/volumes/proxy +SSH_CMD="ssh -o StrictHostKeyChecking=no -o PasswordAuthentication=no -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null" + +mkdir -p ${VOL_DIR}/etc/haproxy/ +rsync -e "${SSH_CMD}" -va --progress root@proxy1.{{ oo_env }}.rhcloud.com:/etc/haproxy/ ${VOL_DIR}/etc/haproxy/ + +mkdir -p ${VOL_DIR}/etc/httpd/ +rsync -e "${SSH_CMD}" -va --progress root@proxy1.{{ oo_env }}.rhcloud.com:/etc/httpd/ ${VOL_DIR}/etc/httpd/ + +mkdir -p ${VOL_DIR}/etc/pki/tls/ +rsync -e "${SSH_CMD}" -va --progress root@proxy1.{{ oo_env }}.rhcloud.com:/etc/pki/tls/ ${VOL_DIR}/etc/pki/tls/ + +# We need to disable the haproxy chroot +sed -i -re 's/^(\s+)chroot/\1#chroot/' /var/lib/docker/volumes/proxy/etc/haproxy/haproxy.cfg diff --git a/roles/atomic_proxy/vars/main.yml b/roles/atomic_proxy/vars/main.yml new file mode 100644 index 000000000..1f90492fd --- /dev/null +++ b/roles/atomic_proxy/vars/main.yml @@ -0,0 +1,2 @@ +--- +oo_proxy_puppet_volume_dir: /var/lib/docker/volumes/proxy_puppet diff --git a/roles/shutdown_nightly/README.md b/roles/shutdown_nightly/README.md new file mode 100644 index 000000000..003f83210 --- /dev/null +++ b/roles/shutdown_nightly/README.md @@ -0,0 +1,56 @@ +Role Name +======== + +The purpose of this role is to do common configurations for all RHEL atomic hosts. + + +Requirements +------------ + +None + + +Role Variables +-------------- + +None + + +Dependencies +------------ + +None + + +Example Playbook +------------------------- + +From a group playbook: + + hosts: servers + roles: + - ../../roles/shutdown_nightly + + +License +------- + +Copyright 2012-2014 Red Hat, Inc., All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +Author Information +------------------ + +Thomas Wiest <twiest@redhat.com> diff --git a/roles/shutdown_nightly/defaults/main.yml b/roles/shutdown_nightly/defaults/main.yml new file mode 100644 index 000000000..e5531dec5 --- /dev/null +++ b/roles/shutdown_nightly/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for role diff --git a/roles/shutdown_nightly/handlers/main.yml b/roles/shutdown_nightly/handlers/main.yml new file mode 100644 index 000000000..a8fb69670 --- /dev/null +++ b/roles/shutdown_nightly/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for role diff --git a/roles/shutdown_nightly/meta/main.yml b/roles/shutdown_nightly/meta/main.yml new file mode 100644 index 000000000..9578ab809 --- /dev/null +++ b/roles/shutdown_nightly/meta/main.yml @@ -0,0 +1,19 @@ +--- +galaxy_info: + author: Thomas Wiest + description: Common base RHEL atomic configurations + company: Red Hat + # Some suggested licenses: + # - BSD (default) + # - MIT + # - GPLv2 + # - GPLv3 + # - Apache + # - CC-BY + license: Apache + min_ansible_version: 1.2 + platforms: + - name: EL + versions: + - 7 +dependencies: [] diff --git a/roles/shutdown_nightly/tasks/main.yml b/roles/shutdown_nightly/tasks/main.yml new file mode 100644 index 000000000..f99811572 --- /dev/null +++ b/roles/shutdown_nightly/tasks/main.yml @@ -0,0 +1,7 @@ +--- +- name: Setup nightly shutdown command to save money + cron: > + name="shutdown system at night to save money" + hour="18" + minute="0" + job="/usr/sbin/shutdown --halt" diff --git a/roles/shutdown_nightly/vars/main.yml b/roles/shutdown_nightly/vars/main.yml new file mode 100644 index 000000000..01ab1e425 --- /dev/null +++ b/roles/shutdown_nightly/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for role |