From caf9b52ac7f964a7d4c302e691fc6a59ecf7bb8c Mon Sep 17 00:00:00 2001 From: Mark Goddard <mark@stackhpc.com> Date: Tue, 28 Feb 2017 16:06:36 +0000 Subject: [PATCH] Add kayobe python module with CLI The CLI replaces all existing shell scripts except for configure-kayobe.sh. Other shell scripts are now removed. --- bootstrap.sh | 55 ----- deploy-overcloud.sh | 71 ------ deploy-seed.sh | 72 ------ kayobe-config-dump | 18 -- kayobe-playbook | 22 -- kayobe/__init__.py | 0 kayobe/ansible.py | 173 ++++++++++++++ kayobe/cli/__init__.py | 0 kayobe/cli/commands.py | 212 ++++++++++++++++++ kayobe/cmd/__init__.py | 0 kayobe/cmd/kayobe.py | 35 +++ kayobe/kolla_ansible.py | 121 ++++++++++ kayobe/utils.py | 68 ++++++ provision-overcloud.sh | 45 ---- provision-seed.sh | 34 --- requirements.txt | 1 + setup.py | 51 +++++ .../configure-kayobe.sh | 0 18 files changed, 661 insertions(+), 317 deletions(-) delete mode 100755 bootstrap.sh delete mode 100755 deploy-overcloud.sh delete mode 100755 deploy-seed.sh delete mode 100755 kayobe-config-dump delete mode 100755 kayobe-playbook create mode 100644 kayobe/__init__.py create mode 100644 kayobe/ansible.py create mode 100644 kayobe/cli/__init__.py create mode 100644 kayobe/cli/commands.py create mode 100644 kayobe/cmd/__init__.py create mode 100644 kayobe/cmd/kayobe.py create mode 100644 kayobe/kolla_ansible.py create mode 100644 kayobe/utils.py delete mode 100755 provision-overcloud.sh delete mode 100755 provision-seed.sh create mode 100644 requirements.txt create mode 100644 setup.py rename configure-kayobe.sh => tools/configure-kayobe.sh (100%) diff --git a/bootstrap.sh b/bootstrap.sh deleted file mode 100755 index 996a7d52..00000000 --- a/bootstrap.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -set -e - -function run_playbook { - KAYOBE_CONFIG_PATH=${KAYOBE_CONFIG_PATH:-/etc/kayobe} - # Ansible fails silently if the inventory does not exist. - test -e ${KAYOBE_CONFIG_PATH}/inventory - ansible-playbook \ - -i ${KAYOBE_CONFIG_PATH}/inventory \ - -e @${KAYOBE_CONFIG_PATH}/globals.yml \ - -e @${KAYOBE_CONFIG_PATH}/dns.yml \ - -e @${KAYOBE_CONFIG_PATH}/kolla.yml \ - -e @${KAYOBE_CONFIG_PATH}/networks.yml \ - -e @${KAYOBE_CONFIG_PATH}/network-allocation.yml \ - -e @${KAYOBE_CONFIG_PATH}/ntp.yml \ - -e @${KAYOBE_CONFIG_PATH}/swift.yml \ - $@ -} - -function install_ansible { - if [[ -f /etc/centos-release ]]; then - sudo yum -y install epel-release - elif [[ -f /etc/redhat-release ]]; then - sudo subscription-manager repos --enable=qci-1.0-for-rhel-7-rpms - if ! yum info epel-release >/dev/null 2>&1 ; then - sudo yum -y install \ - https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm - fi - fi - sudo yum -y install ansible -} - -function install_ansible_roles { - ansible-galaxy install \ - --roles-path ansible/roles \ - --role-file ansible/requirements.yml -} - -function bootstrap { - run_playbook ansible/bootstrap.yml -} - -function install_kolla { - run_playbook ansible/kolla.yml -} - -function main { - install_ansible - install_ansible_roles - bootstrap - install_kolla -} - -main $* diff --git a/deploy-overcloud.sh b/deploy-overcloud.sh deleted file mode 100755 index 4df090ef..00000000 --- a/deploy-overcloud.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -set -e - -function run_playbook { - KAYOBE_CONFIG_PATH=${KAYOBE_CONFIG_PATH:-/etc/kayobe} - # Ansible fails silently if the inventory does not exist. - test -e ${KAYOBE_CONFIG_PATH}/inventory - ansible-playbook \ - -i ${KAYOBE_CONFIG_PATH}/inventory \ - -e @${KAYOBE_CONFIG_PATH}/controllers.yml \ - -e @${KAYOBE_CONFIG_PATH}/dns.yml \ - -e @${KAYOBE_CONFIG_PATH}/globals.yml \ - -e @${KAYOBE_CONFIG_PATH}/kolla.yml \ - -e @${KAYOBE_CONFIG_PATH}/networks.yml \ - -e @${KAYOBE_CONFIG_PATH}/network-allocation.yml \ - -e @${KAYOBE_CONFIG_PATH}/ntp.yml \ - -e @${KAYOBE_CONFIG_PATH}/ssh.yml \ - -e @${KAYOBE_CONFIG_PATH}/swift.yml \ - $@ -} - -function run_kolla_ansible { - export KOLLA_CONFIG_PATH=${KOLLA_CONFIG_PATH:-/etc/kolla} - # Ansible fails silently if the inventory does not exist. - test -e ${KOLLA_CONFIG_PATH}/inventory/overcloud - KOLLA_VENV=$(pwd)/ansible/kolla-venv - source ${KOLLA_VENV}/bin/activate - kolla-ansible \ - --configdir ${KOLLA_CONFIG_PATH} \ - --passwords ${KOLLA_CONFIG_PATH}/passwords.yml \ - -i ${KOLLA_CONFIG_PATH}/inventory/overcloud \ - $@ - deactivate -} - -function configure_os { - ansible_user=$(./kayobe-config-dump -e dump_hosts=controllers[0] -e dump_var_name=kayobe_ansible_user | head -n -1) - run_playbook ansible/ip-allocation.yml -l controllers - run_playbook ansible/ssh-known-host.yml -l controllers - run_playbook ansible/kayobe-ansible-user.yml -l controllers - run_playbook ansible/disable-selinux.yml -l controllers - run_playbook ansible/network.yml -l controllers - run_playbook ansible/ntp.yml -l controllers - run_kolla_ansible bootstrap-servers -e ansible_user=${ansible_user} - run_playbook ansible/kolla-host.yml -l controllers - run_playbook ansible/docker.yml -l controllers -} - -function deploy_services { - run_playbook ansible/kolla-openstack.yml - run_playbook ansible/swift-setup.yml - run_kolla_ansible pull - run_kolla_ansible prechecks - run_kolla_ansible deploy - run_kolla_ansible post-deploy -e node_config_directory=${KOLLA_CONFIG_PATH} -} - -function deploy_overcloud { - configure_os - deploy_services -} - -########################################################### -# Main - -function main { - deploy_overcloud -} - -deploy_overcloud diff --git a/deploy-seed.sh b/deploy-seed.sh deleted file mode 100755 index fb5faf55..00000000 --- a/deploy-seed.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash - -set -e - -function run_playbook { - KAYOBE_CONFIG_PATH=${KAYOBE_CONFIG_PATH:-/etc/kayobe} - # Ansible fails silently if the inventory does not exist. - test -e ${KAYOBE_CONFIG_PATH}/inventory - ansible-playbook \ - -i ${KAYOBE_CONFIG_PATH}/inventory \ - -e @${KAYOBE_CONFIG_PATH}/bifrost.yml \ - -e @${KAYOBE_CONFIG_PATH}/dns.yml \ - -e @${KAYOBE_CONFIG_PATH}/globals.yml \ - -e @${KAYOBE_CONFIG_PATH}/kolla.yml \ - -e @${KAYOBE_CONFIG_PATH}/networks.yml \ - -e @${KAYOBE_CONFIG_PATH}/network-allocation.yml \ - -e @${KAYOBE_CONFIG_PATH}/ntp.yml \ - -e @${KAYOBE_CONFIG_PATH}/seed-vm.yml \ - -e @${KAYOBE_CONFIG_PATH}/ssh.yml \ - -e @${KAYOBE_CONFIG_PATH}/swift.yml \ - $@ -} - -function run_kolla_ansible { - export KOLLA_CONFIG_PATH=${KOLLA_CONFIG_PATH:-/etc/kolla} - # Ansible fails silently if the inventory does not exist. - test -e ${KOLLA_CONFIG_PATH}/inventory/seed - KOLLA_VENV=$(pwd)/ansible/kolla-venv - source ${KOLLA_VENV}/bin/activate - kolla-ansible \ - --configdir ${KOLLA_CONFIG_PATH} \ - --passwords ${KOLLA_CONFIG_PATH}/passwords.yml \ - -i ${KOLLA_CONFIG_PATH}/inventory/seed \ - $@ - deactivate -} - -function configure_os { - ansible_user=$(./kayobe-config-dump -e dump_hosts=seed -e dump_var_name=kayobe_ansible_user | head -n -1) - run_playbook ansible/ip-allocation.yml -l seed - run_playbook ansible/ssh-known-host.yml -l seed - run_playbook ansible/kayobe-ansible-user.yml -l seed - run_playbook ansible/disable-selinux.yml -l seed - run_playbook ansible/network.yml -l seed - run_playbook ansible/ntp.yml -l seed - run_kolla_ansible bootstrap-servers -e ansible_user=${ansible_user} - run_playbook ansible/kolla-host.yml -l seed - run_playbook ansible/docker.yml -l seed -} - -function deploy_bifrost { - # Use a pre-built bifrost image in the stackhpc repository. - # The image was built via kolla-build -t source bifrost-deploy. - run_playbook ansible/kolla-bifrost.yml - run_kolla_ansible deploy-bifrost \ - -e kolla_install_type=source \ - -e docker_namespace=stackhpc -} - -function deploy_seed_node { - configure_os - deploy_bifrost -} - -########################################################### -# Main - -function main { - deploy_seed_node -} - -main $* diff --git a/kayobe-config-dump b/kayobe-config-dump deleted file mode 100755 index 5732fafa..00000000 --- a/kayobe-config-dump +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -dump_dir=$(mktemp -d) - -# Execute the dump-config.yml playbook. -./kayobe-playbook \ - ansible/dump-config.yml \ - -e dump_path=${dump_dir} \ - $@ \ - > /dev/null - -result=$? - -if [[ $result -eq 0 ]]; then - cat ${dump_dir}/*.yml -fi -rm -rf ${dump_dir} -exit $result diff --git a/kayobe-playbook b/kayobe-playbook deleted file mode 100755 index f1d28929..00000000 --- a/kayobe-playbook +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -KAYOBE_CONFIG_PATH=${KAYOBE_CONFIG_PATH:-/etc/kayobe} - -# Ansible fails silently if the inventory does not exist. -test -e ${KAYOBE_CONFIG_PATH}/inventory - -# Execute a Kayobe playbook. -exec ansible-playbook \ - -i ${KAYOBE_CONFIG_PATH}/inventory \ - -e @${KAYOBE_CONFIG_PATH}/bifrost.yml \ - -e @${KAYOBE_CONFIG_PATH}/controllers.yml \ - -e @${KAYOBE_CONFIG_PATH}/dns.yml \ - -e @${KAYOBE_CONFIG_PATH}/globals.yml \ - -e @${KAYOBE_CONFIG_PATH}/kolla.yml \ - -e @${KAYOBE_CONFIG_PATH}/networks.yml \ - -e @${KAYOBE_CONFIG_PATH}/network-allocation.yml \ - -e @${KAYOBE_CONFIG_PATH}/ntp.yml \ - -e @${KAYOBE_CONFIG_PATH}/seed-vm.yml \ - -e @${KAYOBE_CONFIG_PATH}/ssh.yml \ - -e @${KAYOBE_CONFIG_PATH}/swift.yml \ - $@ diff --git a/kayobe/__init__.py b/kayobe/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kayobe/ansible.py b/kayobe/ansible.py new file mode 100644 index 00000000..791e4166 --- /dev/null +++ b/kayobe/ansible.py @@ -0,0 +1,173 @@ +import logging +import os +import os.path +import shutil +import subprocess +import sys +import tempfile + +from kayobe import utils + + +DEFAULT_CONFIG_PATH = "/etc/kayobe" + +CONFIG_PATH_ENV = "KAYOBE_CONFIG_PATH" + +LOG = logging.getLogger(__name__) + + +def galaxy_install(role_file, roles_path): + """Install Ansible roles via Ansible Galaxy.""" + cmd = ["ansible-galaxy", "install"] + cmd += ["--roles-path", roles_path] + cmd += ["--role-file", role_file] + try: + subprocess.check_call(cmd) + except subprocess.CalledProcessError as e: + LOG.error("Failed to install Ansible roles from %s via Ansible " + "Galaxy: returncode %d", role_file, e.returncode) + sys.exit(e.returncode) + + +def add_args(parser): + """Add arguments required for running Ansible playbooks to a parser.""" + default_config_path = os.getenv(CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH) + parser.add_argument("-b", "--become", action="store_true", + help="run operations with become (nopasswd implied)") + parser.add_argument("-C", "--check", action="store_true", + help="don't make any changes; instead, try to predict " + "some of the changes that may occur") + parser.add_argument("--config-path", default=default_config_path, + help="path to Kayobe configuration. " + "(default=$%s or %s)" % + (CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH)) + parser.add_argument("-e", "--extra-vars", metavar="EXTRA_VARS", + action="append", + help="set additional variables as key=value or " + "YAML/JSON") + parser.add_argument("-i", "--inventory", metavar="INVENTORY", + help="specify inventory host path " + "(default=$%s/inventory or %s/inventory) or " + "comma-separated host list" % + (CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH)) + parser.add_argument("-l", "--limit", metavar="SUBSET", + help="further limit selected hosts to an additional " + "pattern") + parser.add_argument("-t", "--tags", metavar="TAGS", action="append", + help="only run plays and tasks tagged with these " + "values") + + +def _get_inventory_path(parsed_args): + """Return the path to the Kayobe inventory.""" + if parsed_args.inventory: + return parsed_args.inventory + else: + return os.path.join(parsed_args.config_path, "inventory") + + +def _validate_args(parsed_args, playbooks): + """Validate Kayobe Ansible arguments.""" + result = utils.is_readable_dir(parsed_args.config_path) + if not result["result"]: + LOG.error("Kayobe configuration path %s is invalid: %s", + parsed_args.config_path, result["message"]) + sys.exit(1) + + inventory = _get_inventory_path(parsed_args) + result = utils.is_readable_file(inventory) + if not result["result"]: + LOG.error("Kayobe inventory %s is invalid: %s", + inventory, result["message"]) + sys.exit(1) + + for playbook in playbooks: + result = utils.is_readable_file(playbook) + if not result["result"]: + LOG.error("Kayobe playbook %s is invalid: %s", + playbook, result["message"]) + sys.exit(1) + + +def build_args(parsed_args, playbooks, + extra_vars=None, limit=None, tags=None): + """Build arguments required for running Ansible playbooks.""" + cmd = ["ansible-playbook"] + inventory = _get_inventory_path(parsed_args) + cmd += ["--inventory", inventory] + for vars_file in os.listdir(parsed_args.config_path): + abs_path = os.path.join(parsed_args.config_path, vars_file) + if os.path.isfile(abs_path): + root, ext = os.path.splitext(vars_file) + if ext in (".yml", ".yaml", ".json"): + cmd += ["-e", "@%s" % abs_path] + if parsed_args.extra_vars: + for extra_var in parsed_args.extra_vars: + cmd += ["-e", extra_var] + if extra_vars: + for extra_var_name, extra_var_value in extra_vars.items(): + cmd += ["-e", "%s=%s" % (extra_var_name, extra_var_value)] + if parsed_args.become: + cmd += ["--become"] + if parsed_args.check: + cmd += ["--check"] + if parsed_args.limit or limit: + limits = [l for l in [parsed_args.limit, limit] if l] + cmd += ["--limit", "&".join(limits)] + if parsed_args.tags or tags: + all_tags = [t for t in [parsed_args.tags, tags] if t] + cmd += ["--tags", ",".join(all_tags)] + cmd += playbooks + return cmd + + +def run_playbooks(parsed_args, playbooks, + extra_vars=None, limit=None, tags=None, quiet=False): + """Run a Kayobe Ansible playbook.""" + _validate_args(parsed_args, playbooks) + cmd = build_args(parsed_args, playbooks, + extra_vars=extra_vars, limit=limit, tags=tags) + try: + utils.run_command(cmd, quiet=quiet) + except subprocess.CalledProcessError as e: + LOG.error("Kayobe playbook(s) %s exited %d", + ", ".join(playbooks), e.returncode) + sys.exit(e.returncode) + + +def run_playbook(parsed_args, playbook, *args, **kwargs): + """Run a Kayobe Ansible playbook.""" + return run_playbooks(parsed_args, [playbook], *args, **kwargs) + + +def config_dump(parsed_args, host=None, hosts=None, var_name=None, + facts=False, extra_vars=None): + dump_dir = tempfile.mkdtemp() + try: + if not extra_vars: + extra_vars = {} + extra_vars["dump_path"] = dump_dir + if host or hosts: + extra_vars["dump_hosts"] = host or hosts + if var_name: + extra_vars["dump_var_name"] = var_name + if facts is not None: + extra_vars["dump_facts"] = facts + run_playbook(parsed_args, "ansible/dump-config.yml", + extra_vars=extra_vars, quiet=True) + hostvars = {} + for path in os.listdir(dump_dir): + LOG.debug("Found dump file %s", path) + inventory_hostname, ext = os.path.splitext(path) + if ext == ".yml": + hvars = utils.read_yaml_file(os.path.join(dump_dir, path)) + if host: + return hvars + else: + hostvars[inventory_hostname] = hvars + else: + LOG.warning("Unexpected extension on config dump file %s", + path) + return hostvars + finally: + shutil.rmtree(dump_dir) diff --git a/kayobe/cli/__init__.py b/kayobe/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py new file mode 100644 index 00000000..e1f4dfeb --- /dev/null +++ b/kayobe/cli/commands.py @@ -0,0 +1,212 @@ +import json +import platform +import sys + +from cliff.command import Command + +from kayobe import ansible +from kayobe import kolla_ansible +from kayobe import utils + + +class KayobeAnsibleMixin(object): + """Mixin class for commands running Kayobe Ansible playbooks.""" + + def get_parser(self, prog_name): + parser = super(KayobeAnsibleMixin, self).get_parser(prog_name) + group = parser.add_argument_group("Kayobe Ansible") + ansible.add_args(group) + return parser + + +class KollaAnsibleMixin(object): + """Mixin class for commands running Kolla Ansible.""" + + def get_parser(self, prog_name): + parser = super(KollaAnsibleMixin, self).get_parser(prog_name) + group = parser.add_argument_group("Kolla Ansible") + kolla_ansible.add_args(group) + return parser + + +class ControlHostBootstrap(KayobeAnsibleMixin, Command): + """Bootstrap the Kayobe control environment.""" + + def take_action(self, parsed_args): + self.app.LOG.debug("Bootstrapping Kayobe control host") + linux_distname = platform.linux_distribution()[0] + if linux_distname == "CentOS Linux": + utils.yum_install(["epel-release"]) + else: + # On RHEL, the following should be done to install EPEL: + # sudo subscription-manager repos --enable=qci-1.0-for-rhel-7-rpms + # if ! yum info epel-release >/dev/null 2>&1 ; then + # sudo yum -y install \ + # https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm + # fi + self.app.LOG.error("%s is not currently supported", linux_distname) + sys.exit(1) + utils.yum_install(["ansible"]) + ansible.galaxy_install("ansible/requirements.yml", "ansible/roles") + playbooks = ["ansible/%s.yml" % playbook for playbook in + "bootstrap", "kolla"] + ansible.run_playbooks(parsed_args, playbooks) + + +class ConfigurationDump(KayobeAnsibleMixin, Command): + """Dump Kayobe configuration.""" + + def get_parser(self, prog_name): + parser = super(ConfigurationDump, self).get_parser(prog_name) + group = parser.add_argument_group("Configuration Dump") + group.add_argument("--dump-facts", default=False, + help="whether to gather and dump host facts") + group.add_argument("--host", + help="name of a host to dump config for") + group.add_argument("--hosts", + help="name of hosts and/or groups to dump config " + "for") + group.add_argument("--var-name", + help="name of a variable to dump") + return parser + + def take_action(self, parsed_args): + self.app.LOG.debug("Dumping Ansible configuration") + hostvars = ansible.config_dump(parsed_args, + host=parsed_args.host, + hosts=parsed_args.hosts, + facts=parsed_args.dump_facts, + var_name=parsed_args.var_name) + try: + json.dump(hostvars, sys.stdout, sort_keys=True, indent=4) + except TypeError as e: + self.app.LOG.error("Failed to JSON encode configuration: %s", + repr(e)) + sys.exit(1) + + +class PlaybookRun(KayobeAnsibleMixin, Command): + """Run a Kayobe Ansible playbook.""" + + def get_parser(self, prog_name): + parser = super(PlaybookRun, self).get_parser(prog_name) + group = parser.add_argument_group("Kayobe Ansible") + group.add_argument("playbook", nargs="+", + help="name of the playbook(s) to run") + return parser + + def take_action(self, parsed_args): + self.app.LOG.debug("Running Kayobe playbook(s)") + ansible.run_playbooks(parsed_args, parsed_args.playbook) + + +class KollaAnsibleRun(KollaAnsibleMixin, Command): + """Run a Kolla Ansible command.""" + + def get_parser(self, prog_name): + parser = super(KollaAnsibleRun, self).get_parser(prog_name) + group = parser.add_argument_group("Kolla Ansible") + group.add_argument("--kolla-inventory-filename", default="overcloud", + choices=["seed", "overcloud"], + help="name of the kolla-ansible inventory file, " + "one of seed or overcloud (default " + "overcloud)") + group.add_argument("command", + help="name of the kolla-ansible command to run") + return parser + + def take_action(self, parsed_args): + self.app.LOG.debug("Running Kolla Ansible command") + kolla_ansible.run(parsed_args, parsed_args.command, + parsed_args.kolla_inventory_filename) + + +class SeedVMProvision(KollaAnsibleMixin, KayobeAnsibleMixin, Command): + """Provision the seed VM.""" + + def take_action(self, parsed_args): + self.app.LOG.debug("Provisioning seed VM") + ansible.run_playbook(parsed_args, "ansible/seed-vm.yml") + + +class SeedDeploy(KollaAnsibleMixin, KayobeAnsibleMixin, Command): + """Deploy the seed node services.""" + + def take_action(self, parsed_args): + self.app.LOG.debug("Deploying seed services") + self._configure_os(parsed_args) + self._deploy_bifrost(parsed_args) + + def _configure_os(self, parsed_args): + ansible_user = ansible.config_dump(parsed_args, host="seed", + var_name="kayobe_ansible_user") + playbooks = ["ansible/%s.yml" % playbook for playbook in + "ip-allocation", "ssh-known-host", "kayobe-ansible-user", + "disable-selinux", "network", "ntp"] + ansible.run_playbooks(parsed_args, playbooks, limit="seed") + kolla_ansible.run_seed(parsed_args, "bootstrap-servers", + extra_vars={"ansible_user": ansible_user}) + playbooks = ["ansible/%s.yml" % playbook for playbook in + "kolla-host", "docker"] + ansible.run_playbooks(parsed_args, playbooks, limit="seed") + + def _deploy_bifrost(self, parsed_args): + ansible.run_playbook(parsed_args, "ansible/kolla-bifrost.yml") + # FIXME: Do this via configuration. + extra_vars = {"kolla_install_type": "source", + "docker_namespace": "stackhpc"} + kolla_ansible.run_seed(parsed_args, "deploy-bifrost", + extra_vars=extra_vars) + + +class OvercloudProvision(KollaAnsibleMixin, KayobeAnsibleMixin, Command): + """Provision the overcloud.""" + + def take_action(self, parsed_args): + self.app.LOG.debug("Provisioning overcloud") + self._configure_network(parsed_args) + self._configure_bios_and_raid(parsed_args) + self._deploy_servers(parsed_args) + + def _configure_network(self, parsed_args): + self.app.LOG.debug("TODO: configure overcloud network") + + def _configure_bios_and_raid(self, parsed_args): + self.app.LOG.debug("TODO: configure overcloud BIOS and RAID") + + def _deploy_servers(self, parsed_args): + self.app.LOG.debug("Deploying overcloud servers via Bifrost") + kolla_ansible.run_seed(parsed_args, "deploy-servers") + + +class OvercloudDeploy(KollaAnsibleMixin, KayobeAnsibleMixin, Command): + """Deploy the overcloud services.""" + + def take_action(self, parsed_args): + self.app.LOG.debug("Deploying overcloud services") + self._configure_os(parsed_args) + self._deploy_services(parsed_args) + + def _configure_os(self, parsed_args): + ansible_user = ansible.config_dump(parsed_args, host="controllers[0]", + var_name="kayobe_ansible_user") + playbooks = ["ansible/%s.yml" % playbook for playbook in + "ip-allocation", "ssh-known-host", "kayobe-ansible-user", + "disable-selinux", "network", "ntp"] + ansible.run_playbooks(parsed_args, playbooks, limit="controllers") + kolla_ansible.run_overcloud(parsed_args, "bootstrap-servers", + extra_vars={"ansible_user": ansible_user}) + playbooks = ["ansible/%s.yml" % playbook for playbook in + "kolla-host", "docker"] + ansible.run_playbooks(parsed_args, playbooks, limit="controllers") + + def _deploy_services(self, parsed_args): + playbooks = ["ansible/%s.yml" % playbook for playbook in + "kolla-openstack", "swift-setup"] + ansible.run_playbooks(parsed_args, playbooks) + for command in ["pull", "prechecks", "deploy"]: + kolla_ansible.run_overcloud(parsed_args, command) + # FIXME: Fudge to work around incorrect configuration path. + extra_vars = {"node_config_directory": parsed_args.config_path} + kolla_ansible.run_overcloud(parsed_args, command, + extra_vars=extra_vars) diff --git a/kayobe/cmd/__init__.py b/kayobe/cmd/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kayobe/cmd/kayobe.py b/kayobe/cmd/kayobe.py new file mode 100644 index 00000000..fe84a072 --- /dev/null +++ b/kayobe/cmd/kayobe.py @@ -0,0 +1,35 @@ +import sys + +from cliff.app import App +from cliff.commandmanager import CommandManager + + +class KayobeApp(App): + + def __init__(self): + super(KayobeApp, self).__init__( + description='Kayobe Command Line Interface (CLI)', + version='0.1', + command_manager=CommandManager('kayobe.cli'), + deferred_help=True, + ) + + def initialize_app(self, argv): + self.LOG.debug('initialize_app') + + def prepare_to_run_command(self, cmd): + self.LOG.debug('prepare_to_run_command %s', cmd.__class__.__name__) + + def clean_up(self, cmd, result, err): + self.LOG.debug('clean_up %s', cmd.__class__.__name__) + if err: + self.LOG.debug('got an error: %s', err) + + +def main(argv=sys.argv[1:]): + myapp = KayobeApp() + return myapp.run(argv) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/kayobe/kolla_ansible.py b/kayobe/kolla_ansible.py new file mode 100644 index 00000000..a56084d8 --- /dev/null +++ b/kayobe/kolla_ansible.py @@ -0,0 +1,121 @@ +import logging +import os +import os.path +import subprocess +import sys + +from kayobe import utils + + +DEFAULT_CONFIG_PATH = "/etc/kolla" + +CONFIG_PATH_ENV = "KOLLA_CONFIG_PATH" + +DEFAULT_VENV_PATH = "ansible/kolla-venv" + +VENV_PATH_ENV = "KOLLA_VENV" + +LOG = logging.getLogger(__name__) + + +def add_args(parser): + """Add arguments required for running Kolla Ansible to a parser.""" + default_config_path = os.getenv(CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH) + default_venv = os.getenv(VENV_PATH_ENV, DEFAULT_VENV_PATH) + parser.add_argument("--kolla-config-path", default=default_config_path, + help="path to Kolla configuration. " + "(default=$%s or %s)" % + (CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH)) + parser.add_argument("--kolla-extra-vars", metavar="EXTRA_VARS", + action="append", + help="set additional variables as key=value or " + "YAML/JSON for Kolla Ansible") + parser.add_argument("--kolla-inventory", metavar="INVENTORY", + help="specify inventory host path " + "(default=$%s/inventory or %s/inventory) or " + "comma-separated host list for Kolla Ansible" % + (CONFIG_PATH_ENV, DEFAULT_CONFIG_PATH)) + parser.add_argument("--kolla-tags", metavar="TAGS", action="append", + help="only run plays and tasks tagged with these " + "values in Kolla Ansible") + parser.add_argument("--kolla-venv", metavar="VENV", default=default_venv, + help="path to virtualenv where Kolla Ansible is " + "installed") + + +def _get_inventory_path(parsed_args, inventory_filename): + """Return the path to the Kolla inventory.""" + if parsed_args.kolla_inventory: + return parsed_args.kolla_inventory + else: + return os.path.join(parsed_args.kolla_config_path, "inventory", + inventory_filename) + + +def _validate_args(parsed_args, inventory_filename): + """Validate Kayobe Ansible arguments.""" + result = utils.is_readable_dir(parsed_args.kolla_config_path) + if not result["result"]: + LOG.error("Kolla configuration path %s is invalid: %s", + parsed_args.kolla_config_path, result["message"]) + sys.exit(1) + + inventory = _get_inventory_path(parsed_args, inventory_filename) + result = utils.is_readable_file(inventory) + if not result["result"]: + LOG.error("Kolla inventory %s is invalid: %s", + inventory, result["message"]) + sys.exit(1) + + result = utils.is_readable_dir(parsed_args.kolla_venv) + if not result["result"]: + LOG.error("Kolla virtualenv %s is invalid: %s", + parsed_args.kolla_venv, result["message"]) + sys.exit(1) + + +def build_args(parsed_args, command, inventory_filename, extra_vars=None, + tags=None): + """Build arguments required for running Kolla Ansible.""" + venv_activate = os.path.join(parsed_args.kolla_venv, "bin", "activate") + cmd = ["source", venv_activate, "&&"] + cmd = ["kolla-ansible", command] + inventory = _get_inventory_path(parsed_args, inventory_filename) + cmd += ["--inventory", inventory] + cmd += ["--configdir", parsed_args.kolla_config_path] + cmd += ["--passwords", + os.path.join(parsed_args.kolla_config_path, "passwords.yml")] + if parsed_args.kolla_extra_vars: + for extra_var in parsed_args.kolla_extra_vars: + cmd += ["-e", extra_var] + if extra_vars: + for extra_var_name, extra_var_value in extra_vars.items(): + cmd += ["-e", "%s=%s" % (extra_var_name, extra_var_value)] + if parsed_args.kolla_tags or tags: + all_tags = [t for t in [parsed_args.kolla_tags, tags] if t] + cmd += ["--tags", ",".join(all_tags)] + return cmd + + +def run(parsed_args, command, inventory_filename, extra_vars=None, + tags=None, quiet=False): + """Run a Kolla Ansible command.""" + _validate_args(parsed_args, inventory_filename) + cmd = build_args(parsed_args, command, + inventory_filename=inventory_filename, + extra_vars=extra_vars, tags=tags) + try: + utils.run_command(" ".join(cmd), quiet=quiet, shell=True) + except subprocess.CalledProcessError as e: + LOG.error("kolla-ansible %s exited %d", command, e.returncode) + sys.exit(e.returncode) + + +def run_seed(*args, **kwargs): + """Run a Kolla Ansible command using the seed inventory.""" + return run(*args, inventory_filename="seed", **kwargs) + + +def run_overcloud(*args, **kwargs): + """Run a Kolla Ansible command using the overcloud inventory.""" + return run(*args, inventory_filename="overcloud", **kwargs) diff --git a/kayobe/utils.py b/kayobe/utils.py new file mode 100644 index 00000000..8475820b --- /dev/null +++ b/kayobe/utils.py @@ -0,0 +1,68 @@ +import logging +import os +import subprocess +import sys +import yaml + + +LOG = logging.getLogger(__name__) + + +def yum_install(packages): + """Install a list of packages via Yum.""" + cmd = ["sudo", "yum", "-y", "install"] + cmd += packages + try: + subprocess.check_call(cmd) + except subprocess.CalledProcessError as e: + print ("Failed to install packages %s via Yum: returncode %d" % + (", ".join(packages), e.returncode)) + sys.exit(e.returncode) + + +def read_yaml_file(path): + """Read and decode a YAML file.""" + try: + with open(path, "r") as f: + content = f.read() + except IOError as e: + print ("Failed to open config dump file %s: %s" % + (path, repr(e))) + sys.exit(1) + try: + return yaml.load(content) + except ValueError as e: + print ("Failed to decode config dump YAML file %s: %s" % + (path, repr(e))) + sys.exit(1) + + +def is_readable_dir(path): + """Check whether a path references a readable directory.""" + if not os.path.exists(path): + return {"result": False, "message": "Path does not exist"} + if not os.path.isdir(path): + return {"result": False, "message": "Path is not a directory"} + if not os.access(path, os.R_OK): + return {"result": False, "message": "Directory is not readable"} + return {"result": True} + + +def is_readable_file(path): + """Check whether a path references a readable file.""" + if not os.path.exists(path): + return {"result": False, "message": "Path does not exist"} + if not os.path.isfile(path): + return {"result": False, "message": "Path is not a file"} + if not os.access(path, os.R_OK): + return {"result": False, "message": "File is not readable"} + return {"result": True} + + +def run_command(cmd, quiet=False, **kwargs): + """Run a command, checking the output.""" + if quiet: + kwargs["stdout"] = subprocess.PIPE + kwargs["stderr"] = subprocess.PIPE + LOG.debug("Running command: %s", " ".join(cmd)) + subprocess.check_call(cmd, **kwargs) diff --git a/provision-overcloud.sh b/provision-overcloud.sh deleted file mode 100755 index 3b2a695a..00000000 --- a/provision-overcloud.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -set -e - -function run_kolla_ansible { - export KOLLA_CONFIG_PATH=${KOLLA_CONFIG_PATH:-/etc/kolla} - # Ansible fails silently if the inventory does not exist. - test -e ${KOLLA_CONFIG_PATH}/inventory/seed - KOLLA_VENV=$(pwd)/ansible/kolla-venv - source ${KOLLA_VENV}/bin/activate - kolla-ansible \ - --configdir ${KOLLA_CONFIG_PATH} \ - --passwords ${KOLLA_CONFIG_PATH}/passwords.yml \ - -i ${KOLLA_CONFIG_PATH}/inventory/seed \ - $@ - deactivate -} - -function configure_network { - echo "TODO: configure overcloud network" -} - -function configure_bios_and_raid { - echo "TODO: configure overcloud BIOS and RAID" -} - -function deploy_servers { - # Deploy servers with Bifrost - run_kolla_ansible deploy-servers -} - -function provision_overcloud { - configure_network - configure_bios_and_raid - deploy_servers -} - -########################################################### -# Main - -function main { - provision_overcloud -} - -provision_overcloud diff --git a/provision-seed.sh b/provision-seed.sh deleted file mode 100755 index c6f55f0c..00000000 --- a/provision-seed.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -set -e - -function run_playbook { - KAYOBE_CONFIG_PATH=${KAYOBE_CONFIG_PATH:-/etc/kayobe} - # Ansible fails silently if the inventory does not exist. - test -e ${KAYOBE_CONFIG_PATH}/inventory - ansible-playbook \ - -i ${KAYOBE_CONFIG_PATH}/inventory \ - -e @${KAYOBE_CONFIG_PATH}/dns.yml \ - -e @${KAYOBE_CONFIG_PATH}/globals.yml \ - -e @${KAYOBE_CONFIG_PATH}/kolla.yml \ - -e @${KAYOBE_CONFIG_PATH}/networks.yml \ - -e @${KAYOBE_CONFIG_PATH}/network-allocation.yml \ - -e @${KAYOBE_CONFIG_PATH}/ntp.yml \ - -e @${KAYOBE_CONFIG_PATH}/seed-vm.yml \ - -e @${KAYOBE_CONFIG_PATH}/ssh.yml \ - -e @${KAYOBE_CONFIG_PATH}/swift.yml \ - $@ -} - -function provision_seed_vm { - run_playbook ansible/seed-vm.yml -} - -########################################################### -# Main - -function main { - provision_seed_vm -} - -main $* diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..1810bb76 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +cliff diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..71f23a2b --- /dev/null +++ b/setup.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +from setuptools import setup, find_packages + + +PROJECT = 'kayobe' +VERSION = '0.1' + +try: + long_description = open('README.md', 'rt').read() +except IOError: + long_description = '' + +setup( + name=PROJECT, + version=VERSION, + + description='OpenStack deployment for scientific computing', + long_description=long_description, + + author='StackHPC', + author_email='mark@stackhpc.com', + + url='https://github.com/stackhpc/kayobe', + download_url='https://github.com/stackhpc/kayobe/tarball/master', + + provides=[], + install_requires=['cliff'], + + namespace_packages=[], + packages=find_packages(), + include_package_data=True, + + entry_points={ + 'console_scripts': [ + 'kayobe = kayobe.cmd.kayobe:main' + ], + 'kayobe.cli': [ + 'control_host_bootstrap = kayobe.cli.commands:ControlHostBootstrap', + 'configuration_dump = kayobe.cli.commands:ConfigurationDump', + 'kolla_ansible_run = kayobe.cli.commands:KollaAnsibleRun', + 'overcloud_deploy = kayobe.cli.commands:OvercloudDeploy', + 'overcloud_provision = kayobe.cli.commands:OvercloudProvision', + 'playbook_run = kayobe.cli.commands:PlaybookRun', + 'seed_deploy = kayobe.cli.commands:SeedDeploy', + 'seed_vm_provision = kayobe.cli.commands:SeedVMProvision', + ], + }, + + zip_safe=False, +) diff --git a/configure-kayobe.sh b/tools/configure-kayobe.sh similarity index 100% rename from configure-kayobe.sh rename to tools/configure-kayobe.sh -- GitLab