Support installing galaxy roles from kayobe-config

Adds support for installing Ansible roles from Galaxy based on a
requirements.yml file in the kayobe configuration repository.

Roles are installed during 'kayobe control host bootstrap' and upgraded
during 'kayobe control host upgrade'. Custom roles are defined in a
requirements file at '$KAYOBE_CONFIG_PATH/ansible/requirements.yml'. The
roles will be installed to '$KAYOBE_CONFIG_PATH/ansible/roles/'.

This forms the basis for supporting customisable extensions to the
standard workflows.

Change-Id: I4cd732623fc26986d5814be487c7930501ac7b7c
Story: 2001663
Task: 12599
This commit is contained in:
Mark Goddard 2018-04-12 12:07:28 +01:00
parent 4ffdd83490
commit 9ec76f9e90
6 changed files with 156 additions and 11 deletions

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import errno
import logging
import os
import os.path
@ -20,6 +21,7 @@ import subprocess
import sys
import tempfile
from kayobe import exception
from kayobe import utils
from kayobe import vault
@ -224,3 +226,39 @@ def config_dump(parsed_args, host=None, hosts=None, var_name=None,
return hostvars
finally:
shutil.rmtree(dump_dir)
def install_galaxy_roles(parsed_args, force=False):
"""Install Ansible Galaxy role dependencies.
Installs dependencies specified in kayobe, and if present, in kayobe
configuration.
:param parsed_args: Parsed command line arguments.
:param force: Whether to force reinstallation of roles.
"""
LOG.info("Installing galaxy role dependencies from kayobe")
utils.galaxy_install("requirements.yml", "ansible/roles", force=force)
# Check for requirements in kayobe configuration.
kc_reqs_path = os.path.join(parsed_args.config_path,
"ansible", "requirements.yml")
if not utils.is_readable_file(kc_reqs_path)["result"]:
LOG.info("Not installing galaxy role dependencies from kayobe config "
"- requirements.yml not present")
return
LOG.info("Installing galaxy role dependencies from kayobe config")
# Ensure a roles directory exists in kayobe-config.
kc_roles_path = os.path.join(parsed_args.config_path,
"ansible", "roles")
try:
os.makedirs(kc_roles_path)
except OSError as e:
if e.errno != errno.EEXIST:
raise exception.Error("Failed to create directory ansible/roles/ "
"in kayobe configuration at %s: %s" %
(parsed_args.config_path, str(e)))
# Install roles from kayobe-config.
utils.galaxy_install(kc_reqs_path, kc_roles_path, force=force)

View File

@ -19,7 +19,6 @@ from cliff.command import Command
from kayobe import ansible
from kayobe import kolla_ansible
from kayobe import utils
from kayobe import vault
@ -120,7 +119,7 @@ class ControlHostBootstrap(KayobeAnsibleMixin, VaultMixin, Command):
def take_action(self, parsed_args):
self.app.LOG.debug("Bootstrapping Kayobe control host")
utils.galaxy_install("requirements.yml", "ansible/roles")
ansible.install_galaxy_roles(parsed_args)
playbooks = _build_playbook_list("bootstrap")
self.run_kayobe_playbooks(parsed_args, playbooks)
playbooks = _build_playbook_list("kolla-ansible")
@ -138,8 +137,7 @@ class ControlHostUpgrade(KayobeAnsibleMixin, VaultMixin, Command):
def take_action(self, parsed_args):
self.app.LOG.debug("Upgrading Kayobe control host")
# Use force to upgrade roles.
utils.galaxy_install("requirements.yml", "ansible/roles",
force=True)
ansible.install_galaxy_roles(parsed_args, force=True)
playbooks = _build_playbook_list("bootstrap")
self.run_kayobe_playbooks(parsed_args, playbooks)
playbooks = _build_playbook_list("kolla-ansible")

21
kayobe/exception.py Normal file
View File

@ -0,0 +1,21 @@
# Copyright (c) 2018 StackHPC Ltd.
#
# 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.
class KayobeException(Exception):
"""Base class for kayobe exceptions."""
class Error(KayobeException):
"""Generic user error."""

View File

@ -18,8 +18,8 @@ import cliff.app
import cliff.commandmanager
import mock
from kayobe import ansible
from kayobe.cli import commands
from kayobe import utils
class TestApp(cliff.app.App):
@ -33,7 +33,7 @@ class TestApp(cliff.app.App):
class TestCase(unittest.TestCase):
@mock.patch.object(utils, "galaxy_install", spec=True)
@mock.patch.object(ansible, "install_galaxy_roles", autospec=True)
@mock.patch.object(commands.KayobeAnsibleMixin,
"run_kayobe_playbooks")
def test_control_host_bootstrap(self, mock_run, mock_install):
@ -42,8 +42,7 @@ class TestCase(unittest.TestCase):
parsed_args = parser.parse_args([])
result = command.run(parsed_args)
self.assertEqual(0, result)
mock_install.assert_called_once_with("requirements.yml",
"ansible/roles")
mock_install.assert_called_once_with(parsed_args)
expected_calls = [
mock.call(mock.ANY, ["ansible/bootstrap.yml"]),
mock.call(mock.ANY, ["ansible/kolla-ansible.yml"],
@ -51,7 +50,7 @@ class TestCase(unittest.TestCase):
]
self.assertEqual(expected_calls, mock_run.call_args_list)
@mock.patch.object(utils, "galaxy_install", spec=True)
@mock.patch.object(ansible, "install_galaxy_roles", autospec=True)
@mock.patch.object(commands.KayobeAnsibleMixin,
"run_kayobe_playbooks")
def test_control_host_upgrade(self, mock_run, mock_install):
@ -60,8 +59,7 @@ class TestCase(unittest.TestCase):
parsed_args = parser.parse_args([])
result = command.run(parsed_args)
self.assertEqual(0, result)
mock_install.assert_called_once_with("requirements.yml",
"ansible/roles", force=True)
mock_install.assert_called_once_with(parsed_args, force=True)
expected_calls = [
mock.call(mock.ANY, ["ansible/bootstrap.yml"]),
mock.call(mock.ANY, ["ansible/kolla-ansible.yml"],

View File

@ -13,6 +13,7 @@
# under the License.
import argparse
import errno
import os
import shutil
import subprocess
@ -22,6 +23,7 @@ import unittest
import mock
from kayobe import ansible
from kayobe import exception
from kayobe import utils
from kayobe import vault
@ -306,6 +308,86 @@ class TestCase(unittest.TestCase):
mock.call(os.path.join(dump_dir, "host2.yml")),
])
@mock.patch.object(utils, 'galaxy_install', autospec=True)
@mock.patch.object(utils, 'is_readable_file', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
def test_install_galaxy_roles(self, mock_mkdirs, mock_is_readable,
mock_install):
parser = argparse.ArgumentParser()
ansible.add_args(parser)
parsed_args = parser.parse_args([])
mock_is_readable.return_value = {"result": False}
ansible.install_galaxy_roles(parsed_args)
mock_install.assert_called_once_with("requirements.yml",
"ansible/roles", force=False)
mock_is_readable.assert_called_once_with(
"/etc/kayobe/ansible/requirements.yml")
self.assertFalse(mock_mkdirs.called)
@mock.patch.object(utils, 'galaxy_install', autospec=True)
@mock.patch.object(utils, 'is_readable_file', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
def test_install_galaxy_roles_with_kayobe_config(
self, mock_mkdirs, mock_is_readable, mock_install):
parser = argparse.ArgumentParser()
ansible.add_args(parser)
parsed_args = parser.parse_args([])
mock_is_readable.return_value = {"result": True}
ansible.install_galaxy_roles(parsed_args)
expected_calls = [
mock.call("requirements.yml", "ansible/roles", force=False),
mock.call("/etc/kayobe/ansible/requirements.yml",
"/etc/kayobe/ansible/roles", force=False)]
self.assertEqual(expected_calls, mock_install.call_args_list)
mock_is_readable.assert_called_once_with(
"/etc/kayobe/ansible/requirements.yml")
mock_mkdirs.assert_called_once_with("/etc/kayobe/ansible/roles")
@mock.patch.object(utils, 'galaxy_install', autospec=True)
@mock.patch.object(utils, 'is_readable_file', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
def test_install_galaxy_roles_with_kayobe_config_forced(
self, mock_mkdirs, mock_is_readable, mock_install):
parser = argparse.ArgumentParser()
ansible.add_args(parser)
parsed_args = parser.parse_args([])
mock_is_readable.return_value = {"result": True}
ansible.install_galaxy_roles(parsed_args, force=True)
expected_calls = [
mock.call("requirements.yml", "ansible/roles", force=True),
mock.call("/etc/kayobe/ansible/requirements.yml",
"/etc/kayobe/ansible/roles", force=True)]
self.assertEqual(expected_calls, mock_install.call_args_list)
mock_is_readable.assert_called_once_with(
"/etc/kayobe/ansible/requirements.yml")
mock_mkdirs.assert_called_once_with("/etc/kayobe/ansible/roles")
@mock.patch.object(utils, 'galaxy_install', autospec=True)
@mock.patch.object(utils, 'is_readable_file', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
def test_install_galaxy_roles_with_kayobe_config_mkdirs_failure(
self, mock_mkdirs, mock_is_readable, mock_install):
parser = argparse.ArgumentParser()
ansible.add_args(parser)
parsed_args = parser.parse_args([])
mock_is_readable.return_value = {"result": True}
mock_mkdirs.side_effect = OSError(errno.EPERM)
self.assertRaises(exception.Error,
ansible.install_galaxy_roles, parsed_args)
mock_install.assert_called_once_with("requirements.yml",
"ansible/roles", force=False)
mock_is_readable.assert_called_once_with(
"/etc/kayobe/ansible/requirements.yml")
mock_mkdirs.assert_called_once_with("/etc/kayobe/ansible/roles")
@mock.patch.object(utils, 'read_file')
def test__read_vault_password_file(self, mock_read):
mock_read.return_value = "test-pass\n"

View File

@ -0,0 +1,8 @@
---
features:
- |
Adds support for installing custom Ansible Galaxy roles during ``kayobe
control host bootstrap`` and ``kayobe control host upgrade``. Custom roles
are defined in a requirements file at
``$KAYOBE_CONFIG_PATH/ansible/requirements.yml``. The roles will be
installed to ``$KAYOBE_CONFIG_PATH/ansible/roles/``.