#!/usr/bin/env python2 # vim: expandtab:tabstop=4:shiftwidth=4 import argparse import ConfigParser import sys import os class Cluster(object): """ Provide Command, Control and Configuration (c3) Interface for OpenShift Clusters """ def __init__(self): # setup ansible ssh environment if 'ANSIBLE_SSH_ARGS' not in os.environ: os.environ['ANSIBLE_SSH_ARGS'] = ( '-o ForwardAgent=yes ' '-o StrictHostKeyChecking=no ' '-o UserKnownHostsFile=/dev/null ' '-o ControlMaster=auto ' '-o ControlPersist=600s ' ) def get_deployment_type(self, args): """ Get the deployment_type based on the environment variables and the command line arguments :param args: command line arguments provided by the user :return: string representing the deployment type """ deployment_type = 'origin' if args.deployment_type: deployment_type = args.deployment_type elif 'OS_DEPLOYMENT_TYPE' in os.environ: deployment_type = os.environ['OS_DEPLOYMENT_TYPE'] return deployment_type def create(self, args): """ Create an OpenShift cluster for given provider :param args: command line arguments provided by user :return: exit status from run command """ env = {'cluster_id': args.cluster_id, 'deployment_type': self.get_deployment_type(args)} playbook = "playbooks/{}/openshift-cluster/launch.yml".format(args.provider) inventory = self.setup_provider(args.provider) env['num_masters'] = args.masters env['num_nodes'] = args.nodes return self.action(args, inventory, env, playbook) def terminate(self, args): """ Destroy OpenShift cluster :param args: command line arguments provided by user :return: exit status from run command """ env = {'cluster_id': args.cluster_id, 'deployment_type': self.get_deployment_type(args)} playbook = "playbooks/{}/openshift-cluster/terminate.yml".format(args.provider) inventory = self.setup_provider(args.provider) return self.action(args, inventory, env, playbook) def list(self, args): """ List VMs in cluster :param args: command line arguments provided by user :return: exit status from run command """ env = {'cluster_id': args.cluster_id, 'deployment_type': self.get_deployment_type(args)} playbook = "playbooks/{}/openshift-cluster/list.yml".format(args.provider) inventory = self.setup_provider(args.provider) return self.action(args, inventory, env, playbook) def config(self, args): """ Configure or reconfigure OpenShift across clustered VMs :param args: command line arguments provided by user :return: exit status from run command """ env = {'cluster_id': args.cluster_id, 'deployment_type': self.get_deployment_type(args)} playbook = "playbooks/{}/openshift-cluster/config.yml".format(args.provider) inventory = self.setup_provider(args.provider) return self.action(args, inventory, env, playbook) def update(self, args): """ Update to latest OpenShift across clustered VMs :param args: command line arguments provided by user :return: exit status from run command """ env = {'cluster_id': args.cluster_id, 'deployment_type': self.get_deployment_type(args)} playbook = "playbooks/{}/openshift-cluster/update.yml".format(args.provider) inventory = self.setup_provider(args.provider) return self.action(args, inventory, env, playbook) def service(self, args): """ Make the same service call across all nodes in the cluster :param args: command line arguments provided by user :return: exit status from run command """ env = {'cluster_id': args.cluster_id, 'deployment_type': self.get_deployment_type(args), 'new_cluster_state': args.state} playbook = "playbooks/{}/openshift-cluster/service.yml".format(args.provider) inventory = self.setup_provider(args.provider) return self.action(args, inventory, env, playbook) def setup_provider(self, provider): """ Setup ansible playbook environment :param provider: command line arguments provided by user :return: path to inventory for given provider """ config = ConfigParser.ConfigParser() if 'gce' == provider: config.readfp(open('inventory/gce/hosts/gce.ini')) for key in config.options('gce'): os.environ[key] = config.get('gce', key) inventory = '-i inventory/gce/hosts' elif 'aws' == provider: config.readfp(open('inventory/aws/hosts/ec2.ini')) for key in config.options('ec2'): os.environ[key] = config.get('ec2', key) inventory = '-i inventory/aws/hosts' elif 'libvirt' == provider: inventory = '-i inventory/libvirt/hosts' elif 'openstack' == provider: inventory = '-i inventory/openstack/hosts' else: # this code should never be reached raise ValueError("invalid PROVIDER {}".format(provider)) return inventory def action(self, args, inventory, env, playbook): """ Build ansible-playbook command line and execute :param args: command line arguments provided by user :param inventory: derived provider library :param env: environment variables for kubernetes :param playbook: ansible playbook to execute :return: exit status from ansible-playbook command """ verbose = '' if args.verbose > 0: verbose = '-{}'.format('v' * args.verbose) if args.option: for opt in args.option: k, v = opt.split('=', 1) env['opt_'+k] = v ansible_env = '-e \'{}\''.format( ' '.join(['%s=%s' % (key, value) for (key, value) in env.items()]) ) command = 'ansible-playbook {} {} {} {}'.format( verbose, inventory, ansible_env, playbook ) if args.verbose > 1: command = 'time {}'.format(command) if args.verbose > 0: sys.stderr.write('RUN [{}]\n'.format(command)) sys.stderr.flush() return os.system(command) if __name__ == '__main__': """ User command to invoke ansible playbooks in a "known" environment Reads ~/.openshift-ansible for default configuration items [DEFAULT] validate_cluster_ids = False cluster_ids = marketing,sales providers = gce,aws,libvirt,openstack """ environment = ConfigParser.SafeConfigParser({ 'cluster_ids': 'marketing,sales', 'validate_cluster_ids': 'False', 'providers': 'gce,aws,libvirt,openstack', }) path = os.path.expanduser("~/.openshift-ansible") if os.path.isfile(path): environment.read(path) cluster = Cluster() parser = argparse.ArgumentParser( description='Python wrapper to ensure proper environment for OpenShift ansible playbooks', ) parser.add_argument('-v', '--verbose', action='count', help='Multiple -v options increase the verbosity') parser.add_argument('--version', action='version', version='%(prog)s 0.3') meta_parser = argparse.ArgumentParser(add_help=False) providers = environment.get('DEFAULT', 'providers').split(',') meta_parser.add_argument('provider', choices=providers, help='provider') if environment.get('DEFAULT', 'validate_cluster_ids').lower() in ("yes", "true", "1"): meta_parser.add_argument('cluster_id', choices=environment.get('DEFAULT', 'cluster_ids').split(','), help='prefix for cluster VM names') else: meta_parser.add_argument('cluster_id', help='prefix for cluster VM names') meta_parser.add_argument('-t', '--deployment-type', choices=['origin', 'online', 'enterprise'], help='Deployment type. (default: origin)') meta_parser.add_argument('-o', '--option', action='append', help='options') action_parser = parser.add_subparsers(dest='action', title='actions', description='Choose from valid actions') create_parser = action_parser.add_parser('create', help='Create a cluster', parents=[meta_parser]) create_parser.add_argument('-m', '--masters', default=1, type=int, help='number of masters to create in cluster') create_parser.add_argument('-n', '--nodes', default=2, type=int, help='number of nodes to create in cluster') create_parser.set_defaults(func=cluster.create) config_parser = action_parser.add_parser('config', help='Configure or reconfigure a cluster', parents=[meta_parser]) config_parser.set_defaults(func=cluster.config) terminate_parser = action_parser.add_parser('terminate', help='Destroy a cluster', parents=[meta_parser]) terminate_parser.add_argument('-f', '--force', action='store_true', help='Destroy cluster without confirmation') terminate_parser.set_defaults(func=cluster.terminate) update_parser = action_parser.add_parser('update', help='Update OpenShift across cluster', parents=[meta_parser]) update_parser.add_argument('-f', '--force', action='store_true', help='Update cluster without confirmation') update_parser.set_defaults(func=cluster.update) list_parser = action_parser.add_parser('list', help='List VMs in cluster', parents=[meta_parser]) list_parser.set_defaults(func=cluster.list) service_parser = action_parser.add_parser('service', help='service for openshift across cluster', parents=[meta_parser]) # choices are the only ones valid for the ansible service module: http://docs.ansible.com/service_module.html service_parser.add_argument('state', choices=['started', 'stopped', 'restarted', 'reloaded'], help='make service call across cluster') service_parser.set_defaults(func=cluster.service) args = parser.parse_args() if 'terminate' == args.action and not args.force: answer = raw_input("This will destroy the ENTIRE {} environment. Are you sure? [y/N] ".format(args.cluster_id)) if answer not in ['y', 'Y']: sys.stderr.write('\nACTION [terminate] aborted by user!\n') exit(1) if 'update' == args.action and not args.force: answer = raw_input( "This is destructive and could corrupt {} environment. Continue? [y/N] ".format(args.cluster_id)) if answer not in ['y', 'Y']: sys.stderr.write('\nACTION [update] aborted by user!\n') exit(1) status = args.func(args) if status != 0: sys.stderr.write("ACTION [{}] failed with exit status {}\n".format(args.action, status)) exit(status)