summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Wiest <twiest@redhat.com>2014-10-22 11:12:46 -0400
committerThomas Wiest <twiest@redhat.com>2014-10-23 12:51:52 -0400
commit5f9c7eb2d2ad44776d33197857dcd0afe693b5f5 (patch)
tree22a39c5589aa3f6a2a01f185041258e4fd69dea6
parent1057c69acdaf47e2bcd4b395069e3bc1bd9eec88 (diff)
downloadopenshift-5f9c7eb2d2ad44776d33197857dcd0afe693b5f5.tar.gz
openshift-5f9c7eb2d2ad44776d33197857dcd0afe693b5f5.tar.bz2
openshift-5f9c7eb2d2ad44776d33197857dcd0afe693b5f5.tar.xz
openshift-5f9c7eb2d2ad44776d33197857dcd0afe693b5f5.zip
Added atomic aws host to cloud.rb
-rwxr-xr-xcloud.rb4
-rw-r--r--inventory/aws/ec2.ini56
-rwxr-xr-xinventory/aws/ec2.py610
-rw-r--r--[-rwxr-xr-x]lib/ansible_helper.rb10
-rw-r--r--lib/aws_command.rb144
-rw-r--r--lib/aws_helper.rb82
-rw-r--r--[-rwxr-xr-x]lib/gce_command.rb12
-rw-r--r--[-rwxr-xr-x]lib/gce_helper.rb16
-rw-r--r--[-rwxr-xr-x]lib/launch_helper.rb4
-rw-r--r--playbooks/aws/os2-atomic-proxy/config.yml21
-rw-r--r--playbooks/aws/os2-atomic-proxy/launch.yml69
-rw-r--r--playbooks/aws/os2-atomic-proxy/user_data.txt6
-rw-r--r--playbooks/aws/os2-atomic-proxy/vars.stg.yml2
-rw-r--r--playbooks/aws/os2-atomic-proxy/vars.yml1
-rw-r--r--roles/atomic_base/README.md56
-rw-r--r--roles/atomic_base/defaults/main.yml2
-rw-r--r--roles/atomic_base/files/bash/bashrc12
-rw-r--r--roles/atomic_base/files/ostree/repo_config10
-rw-r--r--roles/atomic_base/files/system/90-nofile.conf7
-rw-r--r--roles/atomic_base/handlers/main.yml2
-rw-r--r--roles/atomic_base/meta/main.yml19
-rw-r--r--roles/atomic_base/tasks/bash.yml13
-rw-r--r--roles/atomic_base/tasks/cloud_user.yml6
-rw-r--r--roles/atomic_base/tasks/main.yml4
-rw-r--r--roles/atomic_base/tasks/ostree.yml18
-rw-r--r--roles/atomic_base/tasks/system.yml3
-rw-r--r--roles/atomic_base/vars/main.yml2
-rw-r--r--roles/atomic_proxy/README.md56
-rw-r--r--roles/atomic_proxy/defaults/main.yml2
-rw-r--r--roles/atomic_proxy/files/ctr-proxy-1.service37
-rw-r--r--roles/atomic_proxy/files/ctr-proxy-monitoring-1.service37
-rw-r--r--roles/atomic_proxy/files/ctr-proxy-puppet-1.service37
-rw-r--r--roles/atomic_proxy/files/proxy_containers_deploy_descriptor.json29
-rw-r--r--roles/atomic_proxy/files/puppet/auth.conf116
-rwxr-xr-xroles/atomic_proxy/files/setup-proxy-containers.sh43
-rw-r--r--roles/atomic_proxy/handlers/main.yml2
-rw-r--r--roles/atomic_proxy/meta/main.yml21
-rw-r--r--roles/atomic_proxy/tasks/main.yml21
-rw-r--r--roles/atomic_proxy/tasks/setup_puppet.yml24
-rw-r--r--roles/atomic_proxy/templates/puppet/puppet.conf.j240
-rwxr-xr-xroles/atomic_proxy/templates/sync/sync-proxy-configs.sh.j216
-rw-r--r--roles/atomic_proxy/vars/main.yml2
-rw-r--r--roles/shutdown_nightly/README.md56
-rw-r--r--roles/shutdown_nightly/defaults/main.yml2
-rw-r--r--roles/shutdown_nightly/handlers/main.yml2
-rw-r--r--roles/shutdown_nightly/meta/main.yml19
-rw-r--r--roles/shutdown_nightly/tasks/main.yml7
-rw-r--r--roles/shutdown_nightly/vars/main.yml2
48 files changed, 1749 insertions, 13 deletions
diff --git a/cloud.rb b/cloud.rb
index b7ec0790c..934066662 100755
--- a/cloud.rb
+++ b/cloud.rb
@@ -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