From 7eb50cfcf72156df1acfcbc3472ccf3cfdf3794b Mon Sep 17 00:00:00 2001 From: Mark Goddard Date: Tue, 22 Jun 2021 15:10:50 +0000 Subject: [PATCH] Support Ansible collections This change adds support for installing Ansible collections via requirements.yml in Kayobe or Kayobe config. Story: 2008391 Task: 41315 Change-Id: I764ff019a18266b593add7ab80ee095d7d07a869 (cherry picked from commit 5535832c100fb73492d8b4f9b5e611e3b94a2fe1) --- doc/source/contributor/development.rst | 34 +++---- doc/source/custom-ansible-playbooks.rst | 62 ++++++++++-- kayobe/ansible.py | 48 +++++++++- kayobe/cli/commands.py | 4 +- kayobe/tests/unit/cli/test_commands.py | 23 +++-- kayobe/tests/unit/test_ansible.py | 94 ++++++++++++++++++- kayobe/tests/unit/test_utils.py | 63 +++++++++++-- kayobe/utils.py | 25 ++++- .../notes/collections-b1b9a017c843dc1c.yaml | 5 + requirements.yml | 91 +++++++++--------- 10 files changed, 354 insertions(+), 95 deletions(-) create mode 100644 releasenotes/notes/collections-b1b9a017c843dc1c.yaml diff --git a/doc/source/contributor/development.rst b/doc/source/contributor/development.rst index 48b3737fd..6aede7401 100644 --- a/doc/source/contributor/development.rst +++ b/doc/source/contributor/development.rst @@ -38,25 +38,27 @@ in `etc/kayobe/*.yml `__. A number of custom Jinja filters exist in `ansible/filter_plugins/*.py `__. -Kayobe depends on roles hosted on Ansible Galaxy, and these and their version -requirements are defined in `requirements.yml +Kayobe depends on roles and collections hosted on Ansible Galaxy, and these and +their version requirements are defined in `requirements.yml `__. Ansible Galaxy ============== -Kayobe uses a number of Ansible roles hosted on Ansible Galaxy. The role -dependencies are tracked in ``requirements.yml``, and specify required -versions. The process for changing a Galaxy role is as follows: - -#. If required, develop changes for the role. This may be done outside of - Kayobe, or by modifying the role in place during development. If upstream - changes to the role have already been made, this step can be skipped. -#. Commit changes to the role, typically via a Github pull request. -#. Request that a tagged release of the role be made, or make one if you have - the necessary privileges. -#. Ensure that automatic imports are configured for the role using e.g. a - TravisCI webhook notification, or perform a manual import of the role on - Ansible Galaxy. +Kayobe uses a number of Ansible roles and collections hosted on Ansible Galaxy. +The role dependencies are tracked in ``requirements.yml``, and specify required +versions. The process for changing a Galaxy role or collection is as follows: + +#. If required, develop changes for the role or collection. This may be done + outside of Kayobe, or by modifying the code in place during development. If + upstream changes to the code have already been made, this step can be + skipped. +#. Commit changes to the role or collection, typically via a Github pull + request. +#. Request that a tagged release of the role or collection be made, or make one + if you have the necessary privileges. +#. Ensure that automatic imports are configured for the repository using e.g. a + webhook notification, or perform a manual import of the role on Ansible + Galaxy. #. Modify the version in ``requirements.yml`` to match the new release of the - role. + role or collection. diff --git a/doc/source/custom-ansible-playbooks.rst b/doc/source/custom-ansible-playbooks.rst index 6c2c791d0..7941a0f79 100644 --- a/doc/source/custom-ansible-playbooks.rst +++ b/doc/source/custom-ansible-playbooks.rst @@ -75,14 +75,16 @@ These symlinks can even be committed to the kayobe-config Git repository. Ansible Galaxy -------------- -Ansible Galaxy provides a means for sharing Ansible roles. Kayobe -configuration may provide a Galaxy requirements file that defines roles to be -installed from Galaxy. These roles may then be used by custom playbooks. +Ansible Galaxy provides a means for sharing Ansible roles and collections. +Kayobe configuration may provide a Galaxy requirements file that defines roles +and collections to be installed from Galaxy. These roles and collections may +then be used by custom playbooks. -Galaxy role dependencies may be defined in -``$KAYOBE_CONFIG_PATH/ansible/requirements.yml``. These roles will be -installed in ``$KAYOBE_CONFIG_PATH/ansible/roles/`` when bootstrapping the -Ansible control host:: +Galaxy dependencies may be defined in +``$KAYOBE_CONFIG_PATH/ansible/requirements.yml``. These roles and collections +will be installed in ``$KAYOBE_CONFIG_PATH/ansible/roles/`` and +``$KAYOBE_CONFIG_PATH/ansible/collections`` when bootstrapping the Ansible +control host:: (kayobe) $ kayobe control host bootstrap @@ -90,8 +92,8 @@ And updated when upgrading the Ansible control host:: (kayobe) $ kayobe control host upgrade -Example -======= +Example: roles +============== The following example adds a ``foo.yml`` playbook to a set of kayobe configuration. The playbook uses a Galaxy role, ``bar.baz``. @@ -116,7 +118,8 @@ Here is the playbook, ``ansible/foo.yml``:: Here is the Galaxy requirements file, ``ansible/requirements.yml``:: --- - - bar.baz + roles: + - bar.baz We should first install the Galaxy role dependencies, to download the ``bar.baz`` role:: @@ -127,6 +130,45 @@ Then, to run the ``foo.yml`` playbook:: (kayobe) $ kayobe playbook run $KAYOBE_CONFIG_PATH/ansible/foo.yml +Example: collections +==================== + +The following example adds a ``foo.yml`` playbook to a set of kayobe +configuration. The playbook uses a role from a Galaxy collection, +``bar.baz.qux``. + +Here is the kayobe configuration repository structure:: + + etc/kayobe/ + ansible/ + collections/ + foo.yml + requirements.yml + bifrost.yml + ... + +Here is the playbook, ``ansible/foo.yml``:: + + --- + - hosts: controllers + roles: + - name: bar.baz.qux + +Here is the Galaxy requirements file, ``ansible/requirements.yml``:: + + --- + collections: + - bar.baz + +We should first install the Galaxy dependencies, to download the ``bar.baz`` +collection:: + + (kayobe) $ kayobe control host bootstrap + +Then, to run the ``foo.yml`` playbook:: + + (kayobe) $ kayobe playbook run $KAYOBE_CONFIG_PATH/ansible/foo.yml + .. _custom-playbooks-hooks: Hooks diff --git a/kayobe/ansible.py b/kayobe/ansible.py index 6dbc1eb86..69e71b615 100644 --- a/kayobe/ansible.py +++ b/kayobe/ansible.py @@ -291,7 +291,7 @@ def config_dump(parsed_args, host=None, hosts=None, var_name=None, def install_galaxy_roles(parsed_args, force=False): """Install Ansible Galaxy role dependencies. - Installs dependencies specified in kayobe, and if present, in kayobe + Installs role dependencies specified in kayobe, and if present, in kayobe configuration. :param parsed_args: Parsed command line arguments. @@ -300,7 +300,7 @@ def install_galaxy_roles(parsed_args, force=False): LOG.info("Installing galaxy role dependencies from kayobe") requirements = utils.get_data_files_path("requirements.yml") roles_destination = utils.get_data_files_path('ansible', 'roles') - utils.galaxy_install(requirements, roles_destination, force=force) + utils.galaxy_role_install(requirements, roles_destination, force=force) # Check for requirements in kayobe configuration. kc_reqs_path = os.path.join(parsed_args.config_path, @@ -323,7 +323,49 @@ def install_galaxy_roles(parsed_args, force=False): (parsed_args.config_path, str(e))) # Install roles from kayobe-config. - utils.galaxy_install(kc_reqs_path, kc_roles_path, force=force) + utils.galaxy_role_install(kc_reqs_path, kc_roles_path, force=force) + + +def install_galaxy_collections(parsed_args, force=False): + """Install Ansible Galaxy collection dependencies. + + Installs collection 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 collection dependencies from kayobe") + requirements = utils.get_data_files_path("requirements.yml") + collections_destination = utils.get_data_files_path('ansible', + 'collections') + utils.galaxy_collection_install(requirements, collections_destination, + 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 collection dependencies from kayobe " + "config - requirements.yml not present") + return + + LOG.info("Installing galaxy collection dependencies from kayobe config") + # Ensure a collections directory exists in kayobe-config. + kc_collections_path = os.path.join(parsed_args.config_path, + "ansible", "collections") + try: + os.makedirs(kc_collections_path) + except OSError as e: + if e.errno != errno.EEXIST: + raise exception.Error("Failed to create directory " + "ansible/collections/ " + "in kayobe configuration at %s: %s" % + (parsed_args.config_path, str(e))) + + # Install collections from kayobe-config. + utils.galaxy_collection_install(kc_reqs_path, kc_collections_path, + force=force) def prune_galaxy_roles(parsed_args): diff --git a/kayobe/cli/commands.py b/kayobe/cli/commands.py index dba7eab7f..7494be418 100644 --- a/kayobe/cli/commands.py +++ b/kayobe/cli/commands.py @@ -232,6 +232,7 @@ class ControlHostBootstrap(KayobeAnsibleMixin, KollaAnsibleMixin, VaultMixin, def take_action(self, parsed_args): self.app.LOG.debug("Bootstrapping Kayobe Ansible control host") ansible.install_galaxy_roles(parsed_args) + ansible.install_galaxy_collections(parsed_args) playbooks = _build_playbook_list("bootstrap") self.run_kayobe_playbooks(parsed_args, playbooks, ignore_limit=True) @@ -271,8 +272,9 @@ def take_action(self, parsed_args): # Remove roles that are no longer used. Do this before installing new # ones, just in case a custom role dependency includes any. ansible.prune_galaxy_roles(parsed_args) - # Use force to upgrade roles. + # Use force to upgrade roles and collections. ansible.install_galaxy_roles(parsed_args, force=True) + ansible.install_galaxy_collections(parsed_args, force=True) playbooks = _build_playbook_list("bootstrap") self.run_kayobe_playbooks(parsed_args, playbooks, ignore_limit=True) playbooks = _build_playbook_list("kolla-ansible") diff --git a/kayobe/tests/unit/cli/test_commands.py b/kayobe/tests/unit/cli/test_commands.py index 1f8f082ac..e4fc92790 100644 --- a/kayobe/tests/unit/cli/test_commands.py +++ b/kayobe/tests/unit/cli/test_commands.py @@ -35,18 +35,21 @@ def __init__(self): class TestCase(unittest.TestCase): @mock.patch.object(ansible, "install_galaxy_roles", autospec=True) + @mock.patch.object(ansible, "install_galaxy_collections", autospec=True) @mock.patch.object(ansible, "passwords_yml_exists", autospec=True) @mock.patch.object(commands.KayobeAnsibleMixin, "run_kayobe_playbooks") def test_control_host_bootstrap(self, mock_run, mock_passwords, - mock_install): + mock_install_collections, + mock_install_roles): mock_passwords.return_value = False command = commands.ControlHostBootstrap(TestApp(), []) parser = command.get_parser("test") parsed_args = parser.parse_args([]) result = command.run(parsed_args) self.assertEqual(0, result) - mock_install.assert_called_once_with(parsed_args) + mock_install_roles.assert_called_once_with(parsed_args) + mock_install_collections.assert_called_once_with(parsed_args) expected_calls = [ mock.call( mock.ANY, @@ -63,20 +66,23 @@ def test_control_host_bootstrap(self, mock_run, mock_passwords, self.assertEqual(expected_calls, mock_run.call_args_list) @mock.patch.object(ansible, "install_galaxy_roles", autospec=True) + @mock.patch.object(ansible, "install_galaxy_collections", autospec=True) @mock.patch.object(ansible, "passwords_yml_exists", autospec=True) @mock.patch.object(commands.KayobeAnsibleMixin, "run_kayobe_playbooks") @mock.patch.object(commands.KollaAnsibleMixin, "run_kolla_ansible_overcloud") def test_control_host_bootstrap_with_passwords( - self, mock_kolla_run, mock_run, mock_passwords, mock_install): + self, mock_kolla_run, mock_run, mock_passwords, + mock_install_collections, mock_install_roles): mock_passwords.return_value = True command = commands.ControlHostBootstrap(TestApp(), []) parser = command.get_parser("test") parsed_args = parser.parse_args([]) result = command.run(parsed_args) self.assertEqual(0, result) - mock_install.assert_called_once_with(parsed_args) + mock_install_roles.assert_called_once_with(parsed_args) + mock_install_collections.assert_called_once_with(parsed_args) expected_calls = [ mock.call( mock.ANY, @@ -106,16 +112,21 @@ def test_control_host_bootstrap_with_passwords( self.assertEqual(expected_calls, mock_kolla_run.call_args_list) @mock.patch.object(ansible, "install_galaxy_roles", autospec=True) + @mock.patch.object(ansible, "install_galaxy_collections", autospec=True) @mock.patch.object(ansible, "prune_galaxy_roles", autospec=True) @mock.patch.object(commands.KayobeAnsibleMixin, "run_kayobe_playbooks") - def test_control_host_upgrade(self, mock_run, mock_prune, mock_install): + def test_control_host_upgrade(self, mock_run, mock_prune, + mock_install_roles, + mock_install_collections): command = commands.ControlHostUpgrade(TestApp(), []) parser = command.get_parser("test") parsed_args = parser.parse_args([]) result = command.run(parsed_args) self.assertEqual(0, result) - mock_install.assert_called_once_with(parsed_args, force=True) + mock_install_roles.assert_called_once_with(parsed_args, force=True) + mock_install_collections.assert_called_once_with(parsed_args, + force=True) mock_prune.assert_called_once_with(parsed_args) expected_calls = [ mock.call( diff --git a/kayobe/tests/unit/test_ansible.py b/kayobe/tests/unit/test_ansible.py index 4f9d8945f..55da77a2a 100644 --- a/kayobe/tests/unit/test_ansible.py +++ b/kayobe/tests/unit/test_ansible.py @@ -434,7 +434,7 @@ def test_config_dump(self, mock_mkdtemp, mock_run, mock_listdir, mock_read, mock.call(os.path.join(dump_dir, "host2.yml")), ]) - @mock.patch.object(utils, 'galaxy_install', autospec=True) + @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, @@ -453,7 +453,7 @@ def test_install_galaxy_roles(self, mock_mkdirs, mock_is_readable, "/etc/kayobe/ansible/requirements.yml") self.assertFalse(mock_mkdirs.called) - @mock.patch.object(utils, 'galaxy_install', autospec=True) + @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( @@ -476,7 +476,7 @@ def test_install_galaxy_roles_with_kayobe_config( "/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, '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( @@ -499,7 +499,7 @@ def test_install_galaxy_roles_with_kayobe_config_forced( "/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, '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( @@ -520,6 +520,92 @@ def test_install_galaxy_roles_with_kayobe_config_mkdirs_failure( "/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.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/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.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/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() diff --git a/kayobe/tests/unit/test_utils.py b/kayobe/tests/unit/test_utils.py index 9afe62dad..85e983374 100644 --- a/kayobe/tests/unit/test_utils.py +++ b/kayobe/tests/unit/test_utils.py @@ -26,23 +26,72 @@ class TestCase(unittest.TestCase): @mock.patch.object(utils, "run_command") - def test_galaxy_install(self, mock_run): - utils.galaxy_install("/path/to/role/file", "/path/to/roles") - mock_run.assert_called_once_with(["ansible-galaxy", "install", + def test_galaxy_role_install(self, mock_run): + utils.galaxy_role_install("/path/to/role/file", "/path/to/roles") + mock_run.assert_called_once_with(["ansible-galaxy", "role", "install", "--roles-path", "/path/to/roles", "--role-file", "/path/to/role/file"]) @mock.patch.object(utils, "run_command") - def test_galaxy_install_failure(self, mock_run): + def test_galaxy_role_install_failure(self, mock_run): mock_run.side_effect = subprocess.CalledProcessError(1, "command") self.assertRaises(SystemExit, - utils.galaxy_install, "/path/to/role/file", + utils.galaxy_role_install, "/path/to/role/file", "/path/to/roles") + @mock.patch.object(utils, "run_command") + @mock.patch.object(utils, "read_yaml_file") + def test_galaxy_collection_install(self, mock_read, mock_run): + mock_read.return_value = {"collections": []} + utils.galaxy_collection_install("/path/to/collection/file", + "/path/to/collections") + mock_run.assert_called_once_with(["ansible-galaxy", "collection", + "install", "--collections-path", + "/path/to/collections", + "--requirements-file", + "/path/to/collection/file"]) + + @mock.patch.object(utils, "run_command") + @mock.patch.object(utils, "read_yaml_file") + def test_galaxy_collection_install_failure(self, mock_read, mock_run): + mock_read.return_value = {"collections": []} + mock_run.side_effect = subprocess.CalledProcessError(1, "command") + self.assertRaises(SystemExit, + utils.galaxy_collection_install, + "/path/to/collection/file", "/path/to/collections") + + @mock.patch.object(utils, "run_command") + @mock.patch.object(utils, "read_file") + def test_galaxy_collection_read_failure(self, mock_read, mock_run): + mock_read.side_effect = IOError + self.assertRaises(SystemExit, + utils.galaxy_collection_install, + "/path/to/collection/file", "/path/to/collections") + + @mock.patch.object(utils, "run_command") + @mock.patch.object(utils, "read_yaml_file") + def test_galaxy_collection_no_collections(self, mock_read, mock_run): + mock_read.return_value = {"roles": []} + utils.galaxy_collection_install("/path/to/collection/file", + "/path/to/collections") + mock_run.assert_called_once_with(["ansible-galaxy", "collection", + "install", "--collections-path", + "/path/to/collections", + "--requirements-file", + "/path/to/collection/file"]) + + @mock.patch.object(utils, "run_command") + @mock.patch.object(utils, "read_yaml_file") + def test_galaxy_collection_legacy_format(self, mock_read, mock_run): + mock_read.return_value = [] + utils.galaxy_collection_install("/path/to/collection/file", + "/path/to/collections") + self.assertFalse(mock_run.called) + @mock.patch.object(utils, "run_command") def test_galaxy_remove(self, mock_run): utils.galaxy_remove(["role1", "role2"], "/path/to/roles") - mock_run.assert_called_once_with(["ansible-galaxy", "remove", + mock_run.assert_called_once_with(["ansible-galaxy", "role", "remove", "--roles-path", "/path/to/roles", "role1", "role2"]) @@ -50,7 +99,7 @@ def test_galaxy_remove(self, mock_run): def test_galaxy_remove_failure(self, mock_run): mock_run.side_effect = subprocess.CalledProcessError(1, "command") self.assertRaises(SystemExit, - utils.galaxy_install, ["role1", "role2"], + utils.galaxy_remove, ["role1", "role2"], "/path/to/roles") @mock.patch.object(utils, "read_file") diff --git a/kayobe/utils.py b/kayobe/utils.py index deaac12c4..2ded367c4 100644 --- a/kayobe/utils.py +++ b/kayobe/utils.py @@ -72,9 +72,9 @@ def _get_base_path(): return os.path.join(os.path.realpath(__file__), "..") -def galaxy_install(role_file, roles_path, force=False): +def galaxy_role_install(role_file, roles_path, force=False): """Install Ansible roles via Ansible Galaxy.""" - cmd = ["ansible-galaxy", "install"] + cmd = ["ansible-galaxy", "role", "install"] cmd += ["--roles-path", roles_path] cmd += ["--role-file", role_file] if force: @@ -87,10 +87,29 @@ def galaxy_install(role_file, roles_path, force=False): sys.exit(e.returncode) +def galaxy_collection_install(requirements_file, collections_path, + force=False): + requirements = read_yaml_file(requirements_file) + if not isinstance(requirements, dict): + # Handle legacy role list format, which causes the command to fail. + return + cmd = ["ansible-galaxy", "collection", "install"] + cmd += ["--collections-path", collections_path] + cmd += ["--requirements-file", requirements_file] + if force: + cmd += ["--force"] + try: + run_command(cmd) + except subprocess.CalledProcessError as e: + LOG.error("Failed to install Ansible collections from %s via Ansible " + "Galaxy: returncode %d", requirements_file, e.returncode) + sys.exit(e.returncode) + + def galaxy_remove(roles_to_remove, roles_path): """Remove Ansible roles via Ansible Galaxy.""" - cmd = ["ansible-galaxy", "remove"] + cmd = ["ansible-galaxy", "role", "remove"] cmd += ["--roles-path", roles_path] cmd += roles_to_remove try: diff --git a/releasenotes/notes/collections-b1b9a017c843dc1c.yaml b/releasenotes/notes/collections-b1b9a017c843dc1c.yaml new file mode 100644 index 000000000..b235a6049 --- /dev/null +++ b/releasenotes/notes/collections-b1b9a017c843dc1c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Adds support for installing Ansible collections. See `story 2008391 + `__ for details. diff --git a/requirements.yml b/requirements.yml index de29f1049..2c6e8707d 100644 --- a/requirements.yml +++ b/requirements.yml @@ -1,46 +1,47 @@ --- -- src: ahuffman.resolv - version: 1.3.1 -- src: stackhpc.systemd_networkd - version: v1.0.1 -- src: jriguera.configdrive - # There are no versioned releases of this role. - version: e12d38378ae127c9c61d170fa4ba4729f2c5f2ad -- src: MichaelRigart.interfaces - version: v1.12.0 -- src: mrlesmithjr.chrony - version: v0.1.1 -- src: mrlesmithjr.manage-lvm - version: v0.2.2 -- src: mrlesmithjr.mdadm - version: v0.1.1 -- src: singleplatform-eng.users - version: v1.2.5 -- src: stackhpc.dell-powerconnect-switch - version: v1.1.0 -- src: stackhpc.drac - version: 1.1.5 -- src: stackhpc.drac-facts - version: 1.0.0 -- src: stackhpc.grafana-conf - version: 1.1.1 -- src: stackhpc.libvirt-host - version: v1.8.3 -- src: stackhpc.libvirt-vm - version: v1.14.2 -- src: stackhpc.luks - version: 0.4.1 -- src: stackhpc.mellanox-switch - version: v1.0.0 -- src: stackhpc.os-images - version: v1.10.7 -- src: stackhpc.os-ironic-state - version: v1.3.1 -- src: stackhpc.os-networks - version: v1.5.3 -- src: stackhpc.os-openstackclient - version: v1.4.1 -- src: stackhpc.os_openstacksdk - version: v1.0.1 -- src: stackhpc.timezone - version: 1.2.1 +roles: + - src: ahuffman.resolv + version: 1.3.1 + - src: stackhpc.systemd_networkd + version: v1.0.1 + - src: jriguera.configdrive + # There are no versioned releases of this role. + version: e12d38378ae127c9c61d170fa4ba4729f2c5f2ad + - src: MichaelRigart.interfaces + version: v1.12.0 + - src: mrlesmithjr.chrony + version: v0.1.1 + - src: mrlesmithjr.manage-lvm + version: v0.2.2 + - src: mrlesmithjr.mdadm + version: v0.1.1 + - src: singleplatform-eng.users + version: v1.2.5 + - src: stackhpc.dell-powerconnect-switch + version: v1.1.0 + - src: stackhpc.drac + version: 1.1.5 + - src: stackhpc.drac-facts + version: 1.0.0 + - src: stackhpc.grafana-conf + version: 1.1.1 + - src: stackhpc.libvirt-host + version: v1.8.3 + - src: stackhpc.libvirt-vm + version: v1.14.2 + - src: stackhpc.luks + version: 0.4.1 + - src: stackhpc.mellanox-switch + version: v1.0.0 + - src: stackhpc.os-images + version: v1.10.7 + - src: stackhpc.os-ironic-state + version: v1.3.1 + - src: stackhpc.os-networks + version: v1.5.3 + - src: stackhpc.os-openstackclient + version: v1.4.1 + - src: stackhpc.os_openstacksdk + version: v1.0.1 + - src: stackhpc.timezone + version: 1.2.1