# Copyright (c) 2017 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.

import argparse
import errno
import logging
import os
import os.path
import shutil
import subprocess
import tempfile
import unittest
from unittest import mock

from kayobe import ansible
from kayobe import exception
from kayobe import utils
from kayobe import vault


@mock.patch.dict(os.environ, clear=True)
class TestCase(unittest.TestCase):

    maxDiff = None

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_run_playbooks(self, mock_validate, mock_vars, mock_run):
        mock_vars.return_value = ["/etc/kayobe/vars-file1.yml",
                                  "/etc/kayobe/vars-file2.yaml"]
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        parsed_args = parser.parse_args([])
        ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"])
        expected_cmd = [
            "ansible-playbook",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/inventory",
            "-e", "@/etc/kayobe/vars-file1.yml",
            "-e", "@/etc/kayobe/vars-file2.yaml",
            "playbook1.yml",
            "playbook2.yml",
        ]
        home = os.path.expanduser("~")
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "ANSIBLE_ROLES_PATH": ":".join([
                "/etc/kayobe/ansible/roles",
                utils.get_data_files_path("ansible", "roles"),
                home + "/.ansible/roles",
                "/usr/share/ansible/roles",
                "/etc/ansible/roles",
            ]),
            "ANSIBLE_COLLECTIONS_PATH": ":".join([
                "/etc/kayobe/ansible/collections",
                utils.get_data_files_path("ansible", "collections"),
                home + "/.ansible/collections",
                "/usr/share/ansible/collections",
            ]),
            "ANSIBLE_ACTION_PLUGINS": ":".join([
                "/etc/kayobe/ansible/action_plugins",
                utils.get_data_files_path("ansible", "action_plugins"),
                home + "/.ansible/plugins/action",
                "/usr/share/ansible/plugins/action",
            ]),
            "ANSIBLE_FILTER_PLUGINS": ":".join([
                "/etc/kayobe/ansible/filter_plugins",
                utils.get_data_files_path("ansible", "filter_plugins"),
                home + "/.ansible/plugins/filter",
                "/usr/share/ansible/plugins/filter",
            ]),
            "ANSIBLE_TEST_PLUGINS": ":".join([
                "/etc/kayobe/ansible/test_plugins",
                utils.get_data_files_path("ansible", "test_plugins"),
                home + "/.ansible/plugins/test",
                "/usr/share/ansible/plugins/test",
            ]),
        }
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_vars.assert_called_once_with(["/etc/kayobe"])

    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(utils, "is_readable_dir")
    @mock.patch.object(utils, "is_readable_file")
    @mock.patch.object(utils, "run_command")
    def test_reserved_environment(
            self, mock_run, mock_readable,
            mock_readable_file, mock_vars):
        mock_readable_file.return_value = {"result": True}
        mock_readable.return_value = {"result": True}
        mock_vars.return_value = ["/path/to/config/vars-file1.yml",
                                  "/path/to/config/vars-file2.yaml"]
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "--environment", "kayobe",
        ]
        parsed_args = parser.parse_args(args)
        with self.assertLogs(level=logging.ERROR) as ctx:
            self.assertRaises(
                SystemExit, ansible.run_playbooks, parsed_args,
                ["playbook1.yml"]
            )
            exp = "The environment name 'kayobe' is reserved for internal use."
            log_found = any(exp in t for t in ctx.output)
            assert(log_found)

    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(utils, "is_readable_dir")
    @mock.patch.object(utils, "is_readable_file")
    @mock.patch.object(utils, "run_command")
    def test_reserved_environment_negative(
            self, mock_run, mock_readable,
            mock_readable_file, mock_vars):
        mock_readable_file.return_value = {"result": True}
        mock_readable.return_value = {"result": True}
        mock_vars.return_value = ["/path/to/config/vars-file1.yml",
                                  "/path/to/config/vars-file2.yaml"]
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "--environment", "kayobe2",
        ]
        parsed_args = parser.parse_args(args)
        ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"])

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_run_playbooks_all_the_args(self, mock_validate, mock_vars,
                                        mock_run):
        mock_vars.return_value = ["/path/to/config/vars-file1.yml",
                                  "/path/to/config/vars-file2.yaml"]
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "-b",
            "-C",
            "--config-path", "/path/to/config",
            "-D",
            "--environment", "test-env",
            "-e", "ev_name1=ev_value1",
            "-i", "/path/to/inventory",
            "-l", "group1:host",
            "-t", "tag1,tag2",
            "-lt",
        ]
        parsed_args = parser.parse_args(args)
        ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"],
                              verbose_level=2)
        expected_cmd = [
            "ansible-playbook",
            "-vv",
            "--list-tasks",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/path/to/inventory",
            "-e", "@/path/to/config/vars-file1.yml",
            "-e", "@/path/to/config/vars-file2.yaml",
            "-e", "ev_name1=ev_value1",
            "--become",
            "--check",
            "--diff",
            "--limit", "group1:host",
            "--tags", "tag1,tag2",
            "playbook1.yml",
            "playbook2.yml",
        ]
        home = os.path.expanduser("~")
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/path/to/config",
            "KAYOBE_ENVIRONMENT": "test-env",
            "ANSIBLE_ROLES_PATH": ":".join([
                "/path/to/config/ansible/roles",
                utils.get_data_files_path("ansible", "roles"),
                home + "/.ansible/roles",
                "/usr/share/ansible/roles",
                "/etc/ansible/roles",
            ]),
            "ANSIBLE_COLLECTIONS_PATH": ":".join([
                "/path/to/config/ansible/collections",
                utils.get_data_files_path("ansible", "collections"),
                home + "/.ansible/collections",
                "/usr/share/ansible/collections",
            ]),
            "ANSIBLE_ACTION_PLUGINS": ":".join([
                "/path/to/config/ansible/action_plugins",
                utils.get_data_files_path("ansible", "action_plugins"),
                home + "/.ansible/plugins/action",
                "/usr/share/ansible/plugins/action",
            ]),
            "ANSIBLE_FILTER_PLUGINS": ":".join([
                "/path/to/config/ansible/filter_plugins",
                utils.get_data_files_path("ansible", "filter_plugins"),
                home + "/.ansible/plugins/filter",
                "/usr/share/ansible/plugins/filter",
            ]),
            "ANSIBLE_TEST_PLUGINS": ":".join([
                "/path/to/config/ansible/test_plugins",
                utils.get_data_files_path("ansible", "test_plugins"),
                home + "/.ansible/plugins/test",
                "/usr/share/ansible/plugins/test",
            ]),
        }
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_vars.assert_called_once_with(
            ["/path/to/config", "/path/to/config/environments/test-env"])

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    @mock.patch.object(vault, "_ask_vault_pass")
    def test_run_playbooks_all_the_long_args(self, mock_ask, mock_validate,
                                             mock_vars, mock_run):
        mock_vars.return_value = ["/path/to/config/vars-file1.yml",
                                  "/path/to/config/vars-file2.yaml"]
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        mock_ask.return_value = "test-pass"
        args = [
            "--ask-vault-pass",
            "--become",
            "--check",
            "--config-path", "/path/to/config",
            "--diff",
            "--environment", "test-env",
            "--extra-vars", "ev_name1=ev_value1",
            "--inventory", "/path/to/inventory",
            "--limit", "group1:host1",
            "--skip-tags", "tag3,tag4",
            "--tags", "tag1,tag2",
            "--list-tasks",
        ]
        parsed_args = parser.parse_args(args)
        mock_run.return_value = "/path/to/kayobe-vault-password-helper"
        ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"])
        expected_cmd = [
            "ansible-playbook",
            "--list-tasks",
            "--vault-password-file", "/path/to/kayobe-vault-password-helper",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/path/to/inventory",
            "-e", "@/path/to/config/vars-file1.yml",
            "-e", "@/path/to/config/vars-file2.yaml",
            "-e", "ev_name1=ev_value1",
            "--become",
            "--check",
            "--diff",
            "--limit", "group1:host1",
            "--skip-tags", "tag3,tag4",
            "--tags", "tag1,tag2",
            "playbook1.yml",
            "playbook2.yml",
        ]
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/path/to/config",
            "KAYOBE_ENVIRONMENT": "test-env",
            "KAYOBE_VAULT_PASSWORD": "test-pass",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        expected_calls = [
            mock.call(["which", "kayobe-vault-password-helper"],
                      check_output=True, universal_newlines=True),
            mock.call(expected_cmd, check_output=False, quiet=False,
                      env=expected_env)
        ]
        self.assertListEqual(expected_calls, mock_run.mock_calls)
        mock_vars.assert_called_once_with(
            ["/path/to/config", "/path/to/config/environments/test-env"])

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    @mock.patch.object(vault, "update_environment")
    def test_run_playbooks_vault_password_file(self, mock_update,
                                               mock_validate,
                                               mock_vars, mock_run):
        mock_vars.return_value = []
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "--vault-password-file", "/path/to/vault/pw",
        ]
        parsed_args = parser.parse_args(args)
        ansible.run_playbooks(parsed_args, ["playbook1.yml"])
        expected_cmd = [
            "ansible-playbook",
            "--vault-password-file", "/path/to/vault/pw",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/inventory",
            "playbook1.yml",
        ]
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_update.assert_called_once_with(mock.ANY, expected_env)

    @mock.patch.dict(os.environ, {"KAYOBE_VAULT_PASSWORD": "test-pass"},
                     clear=True)
    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_run_playbooks_vault_password_helper(self, mock_validate,
                                                 mock_vars, mock_run):
        mock_vars.return_value = []
        parser = argparse.ArgumentParser()
        mock_run.return_value = "/path/to/kayobe-vault-password-helper"
        ansible.add_args(parser)
        vault.add_args(parser)
        mock_run.assert_called_once_with(
            ["which", "kayobe-vault-password-helper"], check_output=True,
            universal_newlines=True)
        mock_run.reset_mock()
        parsed_args = parser.parse_args([])
        ansible.run_playbooks(parsed_args, ["playbook1.yml"])
        expected_cmd = [
            "ansible-playbook",
            "--vault-password-file", "/path/to/kayobe-vault-password-helper",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/inventory",
            "playbook1.yml",
        ]
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "KAYOBE_VAULT_PASSWORD": "test-pass",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_run_playbooks_vault_ask_and_file(self, mock_validate, mock_vars,
                                              mock_run):
        mock_vars.return_value = []
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "--ask-vault-pass",
            "--vault-password-file", "/path/to/vault/pw",
        ]
        self.assertRaises(SystemExit, parser.parse_args, args)

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_run_playbooks_func_args(self, mock_validate, mock_vars, mock_run):
        mock_vars.return_value = ["/etc/kayobe/vars-file1.yml",
                                  "/etc/kayobe/vars-file2.yaml"]
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "--extra-vars", "ev_name1=ev_value1",
            "--limit", "group1:host1",
            "--tags", "tag1,tag2",
        ]
        parsed_args = parser.parse_args(args)
        kwargs = {
            "extra_vars": {"ev_name2": "ev_value2"},
            "limit": "group2:host2",
            "tags": "tag3,tag4",
            "verbose_level": 0,
            "check": True,
            "diff": True,
        }
        ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"],
                              **kwargs)
        expected_cmd = [
            "ansible-playbook",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/inventory",
            "-e", "@/etc/kayobe/vars-file1.yml",
            "-e", "@/etc/kayobe/vars-file2.yaml",
            "-e", "ev_name1=ev_value1",
            "-e", "ev_name2='ev_value2'",
            "--check",
            "--diff",
            "--limit", "group1:host1:&group2:host2",
            "--tags", "tag1,tag2,tag3,tag4",
            "playbook1.yml",
            "playbook2.yml",
        ]
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_vars.assert_called_once_with(["/etc/kayobe"])

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_run_playbooks_ignore_limit(self, mock_validate, mock_vars,
                                        mock_run):
        mock_vars.return_value = ["/etc/kayobe/vars-file1.yml",
                                  "/etc/kayobe/vars-file2.yaml"]
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "-l", "group1:host",
        ]
        parsed_args = parser.parse_args(args)
        ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"],
                              limit="foo", ignore_limit=True)
        expected_cmd = [
            "ansible-playbook",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/inventory",
            "-e", "@/etc/kayobe/vars-file1.yml",
            "-e", "@/etc/kayobe/vars-file2.yaml",
            "playbook1.yml",
            "playbook2.yml",
        ]
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_vars.assert_called_once_with(["/etc/kayobe"])

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_run_playbooks_list_tasks_arg(self, mock_validate, mock_vars,
                                          mock_run):
        mock_vars.return_value = []
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "--list-tasks",
        ]
        parsed_args = parser.parse_args(args)
        kwargs = {
            "list_tasks": False
        }
        ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"],
                              **kwargs)
        expected_cmd = [
            "ansible-playbook",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/inventory",
            "playbook1.yml",
            "playbook2.yml",
        ]
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_vars.assert_called_once_with(["/etc/kayobe"])

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(utils, "is_readable_file")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_run_playbooks_ansible_cfg(self, mock_validate, mock_vars,
                                       mock_readable, mock_run):
        mock_vars.return_value = []
        mock_readable.return_value = {"result": True}
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        parsed_args = parser.parse_args([])
        ansible.run_playbooks(parsed_args, ["playbook1.yml"])
        expected_cmd = [
            "ansible-playbook",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/inventory",
            "playbook1.yml",
        ]
        expected_env = {
            "ANSIBLE_CONFIG": "/etc/kayobe/ansible.cfg",
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_vars.assert_called_once_with(["/etc/kayobe"])
        mock_readable.assert_called_once_with("/etc/kayobe/ansible.cfg")

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(utils, "is_readable_file")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_run_playbooks_ansible_cfg_env(self, mock_validate, mock_vars,
                                           mock_readable, mock_run):
        mock_vars.return_value = []
        mock_readable.return_value = {"result": True}
        os.environ["ANSIBLE_CONFIG"] = "/path/to/ansible.cfg"
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        parsed_args = parser.parse_args([])
        ansible.run_playbooks(parsed_args, ["playbook1.yml"])
        expected_cmd = [
            "ansible-playbook",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/inventory",
            "playbook1.yml",
        ]
        expected_env = {
            "ANSIBLE_CONFIG": "/path/to/ansible.cfg",
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_vars.assert_called_once_with(["/etc/kayobe"])
        mock_readable.assert_called_once_with("/etc/kayobe/ansible.cfg")

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_run_playbooks_failure(self, mock_validate, mock_vars, mock_run):
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        parsed_args = parser.parse_args([])
        mock_run.side_effect = subprocess.CalledProcessError(1, "dummy")
        self.assertRaises(SystemExit,
                          ansible.run_playbooks, parsed_args, ["command"])

    @mock.patch.object(shutil, 'rmtree')
    @mock.patch.object(utils, 'read_yaml_file')
    @mock.patch.object(os, 'listdir')
    @mock.patch.object(ansible, 'run_playbook')
    @mock.patch.object(tempfile, 'mkdtemp')
    def test_config_dump(self, mock_mkdtemp, mock_run, mock_listdir, mock_read,
                         mock_rmtree):
        parser = argparse.ArgumentParser()
        parsed_args = parser.parse_args([])
        dump_dir = "/path/to/dump"
        mock_mkdtemp.return_value = dump_dir
        mock_listdir.return_value = ["host1.yml", "host2.yml"]
        mock_read.side_effect = [
            {"var1": "value1"},
            {"var2": "value2"}
        ]
        result = ansible.config_dump(parsed_args)
        expected_result = {
            "host1": {"var1": "value1"},
            "host2": {"var2": "value2"},
        }
        self.assertEqual(result, expected_result)
        dump_config_path = utils.get_data_files_path(
            "ansible", "dump-config.yml")
        mock_run.assert_called_once_with(parsed_args,
                                         dump_config_path,
                                         extra_vars={
                                             "dump_path": dump_dir,
                                         },
                                         check_output=True, tags=None,
                                         verbose_level=None, check=False,
                                         list_tasks=False, diff=False)
        mock_rmtree.assert_called_once_with(dump_dir)
        mock_listdir.assert_any_call(dump_dir)
        mock_read.assert_has_calls([
            mock.call(os.path.join(dump_dir, "host1.yml")),
            mock.call(os.path.join(dump_dir, "host2.yml")),
        ])

    @mock.patch.object(utils, 'galaxy_role_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(utils.get_data_files_path(
            "requirements.yml"), utils.get_data_files_path(
            "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_role_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(utils.get_data_files_path("requirements.yml"),
                      utils.get_data_files_path("ansible", "roles"),
                      force=False),
            mock.call("/etc/kayobe/ansible/requirements.yml",
                      "/etc/kayobe/ansible/roles", force=False)]
        self.assertListEqual(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_role_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(utils.get_data_files_path("requirements.yml"),
                      utils.get_data_files_path("ansible", "roles"),
                      force=True),
            mock.call("/etc/kayobe/ansible/requirements.yml",
                      "/etc/kayobe/ansible/roles", force=True)]
        self.assertListEqual(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_role_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(utils.get_data_files_path(
            "requirements.yml"), utils.get_data_files_path("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, 'galaxy_collection_install', autospec=True)
    @mock.patch.object(utils, 'is_readable_file', autospec=True)
    @mock.patch.object(os, 'makedirs', autospec=True)
    def test_install_galaxy_collections(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_collections(parsed_args)

        mock_install.assert_called_once_with(utils.get_data_files_path(
            "requirements.yml"), utils.get_data_files_path(
            "ansible", "collections"), force=False)
        mock_is_readable.assert_called_once_with(
            "/etc/kayobe/ansible/requirements.yml")
        self.assertFalse(mock_mkdirs.called)

    @mock.patch.object(utils, 'galaxy_collection_install', autospec=True)
    @mock.patch.object(utils, 'is_readable_file', autospec=True)
    @mock.patch.object(os, 'makedirs', autospec=True)
    def test_install_galaxy_collections_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_collections(parsed_args)

        expected_calls = [
            mock.call(utils.get_data_files_path("requirements.yml"),
                      utils.get_data_files_path("ansible", "collections"),
                      force=False),
            mock.call("/etc/kayobe/ansible/requirements.yml",
                      "/etc/kayobe/ansible/collections", force=False)]
        self.assertListEqual(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/collections")

    @mock.patch.object(utils, 'galaxy_collection_install', autospec=True)
    @mock.patch.object(utils, 'is_readable_file', autospec=True)
    @mock.patch.object(os, 'makedirs', autospec=True)
    def test_install_galaxy_collections_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_collections(parsed_args, force=True)

        expected_calls = [
            mock.call(utils.get_data_files_path("requirements.yml"),
                      utils.get_data_files_path("ansible", "collections"),
                      force=True),
            mock.call("/etc/kayobe/ansible/requirements.yml",
                      "/etc/kayobe/ansible/collections", force=True)]
        self.assertListEqual(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/collections")

    @mock.patch.object(utils, 'galaxy_collection_install', autospec=True)
    @mock.patch.object(utils, 'is_readable_file', autospec=True)
    @mock.patch.object(os, 'makedirs', autospec=True)
    def test_install_galaxy_collections_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_collections, parsed_args)

        mock_install.assert_called_once_with(
            utils.get_data_files_path("requirements.yml"),
            utils.get_data_files_path("ansible", "collections"), force=False)
        mock_is_readable.assert_called_once_with(
            "/etc/kayobe/ansible/requirements.yml")
        mock_mkdirs.assert_called_once_with("/etc/kayobe/ansible/collections")

    @mock.patch.object(utils, 'galaxy_remove', autospec=True)
    def test_prune_galaxy_roles(self, mock_remove):
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        parsed_args = parser.parse_args([])

        ansible.prune_galaxy_roles(parsed_args)

        expected_roles = [
            'resmo.ntp',
            'stackhpc.ntp',
            'stackhpc.os-shade',
            'yatesr.timezone',
        ]
        mock_remove.assert_called_once_with(expected_roles,
                                            "ansible/roles")

    @mock.patch.object(utils, 'is_readable_file', autospec=True)
    def test_passwords_yml_exists_false(self, mock_is_readable):
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        parsed_args = parser.parse_args([])
        mock_is_readable.return_value = {"result": False}

        result = ansible.passwords_yml_exists(parsed_args)

        self.assertFalse(result)
        mock_is_readable.assert_called_once_with(
            "/etc/kayobe/kolla/passwords.yml")

    @mock.patch.object(utils, 'is_readable_file', autospec=True)
    def test_passwords_yml_exists_true(self, mock_is_readable):
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        parsed_args = parser.parse_args(["--config-path", "/path/to/config"])
        mock_is_readable.return_value = {"result": True}

        result = ansible.passwords_yml_exists(parsed_args)

        self.assertTrue(result)
        mock_is_readable.assert_called_once_with(
            "/path/to/config/kolla/passwords.yml")

    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_multiple_inventory_args(self, mock_validate, mock_vars, mock_run):
        mock_vars.return_value = []
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "--inventory", "/etc/kayobe/inventory",
            "--inventory", "/etc/kayobe/environments/foobar/inventory",
        ]
        parsed_args = parser.parse_args(args)
        ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"])
        expected_cmd = [
            "ansible-playbook",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/inventory",
            "--inventory", "/etc/kayobe/environments/foobar/inventory",
            "playbook1.yml",
            "playbook2.yml",
        ]
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_vars.assert_called_once_with(["/etc/kayobe"])

    @mock.patch.object(os.path, "exists")
    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_multiple_inventories(self, mock_validate, mock_vars, mock_run,
                                  mock_exists):
        mock_vars.return_value = []
        # os.path.exists gets called three times:
        # 1) shared inventory
        # 2) environment inventory
        # 3) ansible.cfg
        mock_exists.side_effect = [True, True, False]
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "--environment", "test-env",
        ]
        parsed_args = parser.parse_args(args)
        ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"])
        expected_cmd = [
            "ansible-playbook",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/inventory",
            "--inventory", "/etc/kayobe/environments/test-env/inventory",
            "playbook1.yml",
            "playbook2.yml",
        ]
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "KAYOBE_ENVIRONMENT": "test-env",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        expected_calls = [
            mock.call("/etc/kayobe/inventory"),
            mock.call("/etc/kayobe/environments/test-env/inventory"),
            mock.call("/etc/kayobe/ansible.cfg"),
        ]
        self.assertListEqual(expected_calls, mock_exists.mock_calls)
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_vars.assert_called_once_with(
            ["/etc/kayobe", "/etc/kayobe/environments/test-env"])

    @mock.patch.object(os.path, "exists")
    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_shared_inventory_only(self, mock_validate, mock_vars, mock_run,
                                   mock_exists):
        mock_vars.return_value = []
        # os.path.exists gets called three times:
        # 1) shared inventory
        # 2) environment inventory
        # 3) ansible.cfg
        mock_exists.side_effect = [True, False, False]
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "--environment", "test-env",
        ]
        parsed_args = parser.parse_args(args)
        ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"])
        expected_cmd = [
            "ansible-playbook",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/inventory",
            "playbook1.yml",
            "playbook2.yml",
        ]
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "KAYOBE_ENVIRONMENT": "test-env",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        expected_calls = [
            mock.call("/etc/kayobe/inventory"),
            mock.call("/etc/kayobe/environments/test-env/inventory"),
            mock.call("/etc/kayobe/ansible.cfg"),
        ]
        self.assertListEqual(expected_calls, mock_exists.mock_calls)
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_vars.assert_called_once_with(
            ["/etc/kayobe", "/etc/kayobe/environments/test-env"])

    @mock.patch.object(os.path, "exists")
    @mock.patch.object(utils, "run_command")
    @mock.patch.object(ansible, "_get_vars_files")
    @mock.patch.object(ansible, "_validate_args")
    def test_env_inventory_only(self, mock_validate, mock_vars, mock_run,
                                mock_exists):
        mock_vars.return_value = []
        # os.path.exists gets called three times:
        # 1) shared inventory
        # 2) environment inventory
        # 3) ansible.cfg
        mock_exists.side_effect = [False, True, False]
        parser = argparse.ArgumentParser()
        ansible.add_args(parser)
        vault.add_args(parser)
        args = [
            "--environment", "test-env",
        ]
        parsed_args = parser.parse_args(args)
        ansible.run_playbooks(parsed_args, ["playbook1.yml", "playbook2.yml"])
        expected_cmd = [
            "ansible-playbook",
            "--inventory", utils.get_data_files_path("ansible", "inventory"),
            "--inventory", "/etc/kayobe/environments/test-env/inventory",
            "playbook1.yml",
            "playbook2.yml",
        ]
        expected_env = {
            "KAYOBE_CONFIG_PATH": "/etc/kayobe",
            "KAYOBE_ENVIRONMENT": "test-env",
            "ANSIBLE_ROLES_PATH": mock.ANY,
            "ANSIBLE_COLLECTIONS_PATH": mock.ANY,
            "ANSIBLE_ACTION_PLUGINS": mock.ANY,
            "ANSIBLE_FILTER_PLUGINS": mock.ANY,
            "ANSIBLE_TEST_PLUGINS": mock.ANY,
        }
        expected_calls = [
            mock.call("/etc/kayobe/inventory"),
            mock.call("/etc/kayobe/environments/test-env/inventory"),
            mock.call("/etc/kayobe/ansible.cfg"),
        ]
        self.assertListEqual(expected_calls, mock_exists.mock_calls)
        mock_run.assert_called_once_with(expected_cmd, check_output=False,
                                         quiet=False, env=expected_env)
        mock_vars.assert_called_once_with(
            ["/etc/kayobe", "/etc/kayobe/environments/test-env"])
