Source code for elasticluster.providers.ansible_provider

#
# Copyright (C) 2013 GC3, University of Zurich
#
# This program 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
#
__author__ = 'Nicolas Baer <nicolas.baer@uzh.ch>, Antonio Messina <antonio.s.messina@gmail.com>'

# system imports
import logging
import os
import tempfile
import shutil
import sys

# external imports

# ansible.utils needs to be imported *before* ansible.callbacks!
import ansible.utils
import ansible.callbacks as anscb
import ansible.constants as ansible_constants
from ansible.errors import AnsibleError
from ansible.playbook import PlayBook

# Elasticluster imports
import elasticluster
from elasticluster import log
from elasticluster.providers import AbstractSetupProvider


class ElasticlusterPbCallbacks(anscb.PlaybookCallbacks):
    def on_no_hosts_matched(self):
        anscb.call_callback_module('playbook_on_no_hosts_matched')

    def on_no_hosts_remaining(self):
        anscb.call_callback_module('playbook_on_no_hosts_remaining')

    def on_task_start(self, name, is_conditional):
        if hasattr(self, 'step') and self.step:
            resp = raw_input('Perform task: %s (y/n/c): ' % name)
            if resp.lower() in ['y', 'yes']:
                self.skip_task = False
            elif resp.lower() in ['c', 'continue']:
                self.skip_task = False
                self.step = False
            else:
                self.skip_task = True

        anscb.call_callback_module('playbook_on_task_start', name, is_conditional)

    def on_setup(self):
        anscb.call_callback_module('playbook_on_setup')

    def on_import_for_host(self, host, imported_file):
        anscb.call_callback_module('playbook_on_import_for_host',
                             host, imported_file)

    def on_not_import_for_host(self, host, missing_file):
        anscb.call_callback_module('playbook_on_not_import_for_host',
                             host, missing_file)

    def on_play_start(self, pattern):
        anscb.call_callback_module('playbook_on_play_start', pattern)

    def on_stats(self, stats):
        anscb.call_callback_module('playbook_on_stats', stats)


[docs]class AnsibleSetupProvider(AbstractSetupProvider): """This implementation uses ansible to configure and manage the cluster setup. See https://github.com/ansible/ansible for details. :param dict groups: dictionary of node kinds with corresponding ansible groups to install on the node kind. e.g [node_kind] = ['ansible_group1', 'ansible_group2'] The group defined here can be references in each node. Therefore groups can make it easier to define multiple groups for one node. :param str playbook_path: path to playbook; if empty this will use the shared playbook of elasticluster :param dict environment_vars: dictonary to define variables per node kind, e.g. [node_kind][var] = value :param str storage_path: path to store the inventory file. By default the inventory file is saved temporarily in a temporary directory and deleted when the cluster in stopped. :param bool sudo: indication whether use sudo to gain root permission :param str sudo_user: user with root permission :param str ansible_module_dir: path to addition ansible modules :param extra_conf: tbd. :ivar groups: node kind and ansible group mapping dictionary :ivar environment: additional environment variables """ inventory_file_ending = 'ansible-inventory' def __init__(self, groups, playbook_path=None, environment_vars=dict(), storage_path=None, sudo=True, sudo_user='root', ansible_module_dir=None, **extra_conf): self.groups = groups self._playbook_path = playbook_path self.environment = environment_vars self._storage_path = storage_path self._sudo_user = sudo_user self._sudo = sudo self.extra_conf = extra_conf if not self._playbook_path: self._playbook_path = os.path.join(sys.prefix, 'share/elasticluster/providers/ansible-playbooks', 'site.yml') else: self._playbook_path = os.path.expanduser(self._playbook_path) self._playbook_path = os.path.expandvars(self._playbook_path) if self._storage_path: self._storage_path = os.path.expanduser(self._storage_path) self._storage_path = os.path.expandvars(self._storage_path) self._storage_path_tmp = False if not os.path.exists(self._storage_path): os.makedirs(self._storage_path) else: self._storage_path = tempfile.mkdtemp() self._storage_path_tmp = True if ansible_module_dir: for mdir in ansible_module_dir.split(','): ansible.utils.module_finder.add_directory(mdir.strip())
[docs] def setup_cluster(self, cluster): """Configures the cluster according to the node_kind to ansible group matching. This method is idempotent and therefore can be called multiple times without corrupting the cluster configuration. :param cluster: cluster to configure :type cluster: :py:class:`elasticluster.cluster.Cluster` :return: True on success, False otherwise. Please note, if nothing has to be configures True is returned :raises: `AnsibleError` if the playbook can not be found or playbook is corrupt """ inventory_path = self._build_inventory(cluster) private_key_file = cluster.user_key_private # update ansible constants ansible_constants.HOST_KEY_CHECKING = False ansible_constants.DEFAULT_PRIVATE_KEY_FILE = private_key_file ansible_constants.DEFAULT_SUDO_USER = self._sudo_user # check paths if not inventory_path: # No inventory file has been created, maybe an # invalid calss has been specified in config file? Or none? # assume it is fine. elasticluster.log.info("No setup required for this cluster.") return True if not os.path.exists(inventory_path): raise AnsibleError( "inventory file `%s` could not be found" % inventory_path) # ANTONIO: These should probably be configuration error # instead, and should probably checked inside __init__(). if not os.path.exists(self._playbook_path): raise AnsibleError( "playbook `%s` could not be found" % self._playbook_path) if not os.path.isfile(self._playbook_path): raise AnsibleError( "the playbook `%s` is not a file" % self._playbook_path) elasticluster.log.debug("Using playbook file %s.", self._playbook_path) stats = anscb.AggregateStats() playbook_cb = ElasticlusterPbCallbacks(verbose=0) runner_cb = anscb.DefaultRunnerCallbacks() if elasticluster.log.level <= logging.INFO: playbook_cb = anscb.PlaybookCallbacks() runner_cb = anscb.PlaybookRunnerCallbacks(stats) pb = PlayBook( playbook=self._playbook_path, host_list=inventory_path, callbacks=playbook_cb, runner_callbacks=runner_cb, forks=10, stats=stats, sudo=self._sudo, sudo_user=self._sudo_user, private_key_file=private_key_file, ) try: status = pb.run() except AnsibleError as e: elasticluster.log.error( "could not execute ansible playbooks. message=`%s`", str(e)) return False # Check ansible status. cluster_failures = False for host, hoststatus in status.items(): if hoststatus['unreachable']: elasticluster.log.error( "Host `%s` is unreachable, " "please re-run elasticluster setup", host) cluster_failures = True if hoststatus['failures']: elasticluster.log.error( "Host `%s` had %d failures: please re-run elasticluster " "setup or check the Ansible playbook `%s`" % ( host, hoststatus['failures'], self._playbook_path)) cluster_failures = True if not cluster_failures: elasticluster.log.info("Cluster correctly configured.") # ANTONIO: TODO: We should return an object to identify if # the cluster was correctly configured, if we had # temporary errors or permanent errors. return True return False
def _build_inventory(self, cluster): """Builds the inventory for the given cluster and returns its path :param cluster: cluster to build inventory for :type cluster: :py:class:`elasticluster.cluster.Cluster` """ inventory = dict() for node in cluster.get_all_nodes(): if node.kind in self.groups: extra_vars = ['ansible_ssh_user=%s' % node.image_user] if node.kind in self.environment: extra_vars.extend('%s=%s' % (k, v) for k, v in self.environment[node.kind].items()) for group in self.groups[node.kind]: if group not in inventory: inventory[group] = [] public_ip = node.preferred_ip inventory[group].append( (node.name, public_ip, str.join(' ', extra_vars))) if inventory: # create a temporary file to pass to ansible, since the # api is not stable yet... if self._storage_path_tmp: if not self._storage_path: self._storage_path = tempfile.mkdtemp() elasticluster.log.warning("Writing inventory file to tmp dir " "`%s`", self._storage_path) fname = '%s.%s' % (AnsibleSetupProvider.inventory_file_ending, cluster.name) inventory_path = os.path.join(self._storage_path, fname) inventory_fd = open(inventory_path, 'w+') for section, hosts in inventory.items(): inventory_fd.write("\n[" + section + "]\n") if hosts: for host in hosts: hostline = "%s ansible_ssh_host=%s %s\n" \ % host inventory_fd.write(hostline) inventory_fd.close() return inventory_path else: elasticluster.log.info("No inventory file was created.") return None
[docs] def cleanup(self, cluster): """Deletes the inventory file used last recently used. :param cluster: cluster to clear up inventory file for :type cluster: :py:class:`elasticluster.cluster.Cluster` """ if self._storage_path and os.path.exists(self._storage_path): fname = '%s.%s' % (AnsibleSetupProvider.inventory_file_ending, cluster.name) inventory_path = os.path.join(self._storage_path, fname) if os.path.exists(inventory_path): try: os.unlink(inventory_path) if self._storage_path_tmp: if len(os.listdir(self._storage_path)) == 0: shutil.rmtree(self._storage_path) except OSError, ex: log.warning( "AnsibileProvider: Ignoring error while deleting " "inventory file %s: %s", inventory_path, ex)