From d42594468838128ab02d68c53f55f51828aaade6 Mon Sep 17 00:00:00 2001 From: Gaurav Talreja Date: Wed, 14 Jun 2023 14:24:48 +0530 Subject: [PATCH] Add and remove ansible_role to host/hostgroup (#927) (#929) * Add and remove ansible_role to host/hostgroup Signed-off-by: Gaurav Talreja * Add unittests for add and remove ansible_roles Signed-off-by: Gaurav Talreja --------- Signed-off-by: Gaurav Talreja (cherry picked from commit 183828af355d93e3bff80d2be443875f1850e88b) --- nailgun/entities.py | 137 ++++++++++++++++++++++++++++++++++++++++- tests/test_entities.py | 120 ++++++++++++++++++++++++++++-------- 2 files changed, 230 insertions(+), 27 deletions(-) diff --git a/nailgun/entities.py b/nailgun/entities.py index 77cc467e..440949ff 100644 --- a/nailgun/entities.py +++ b/nailgun/entities.py @@ -3585,11 +3585,17 @@ def path(self, which=None): /api/hostgroups/:hostgroup_id/rebuild_config smart_class_parameters /api/hostgroups/:hostgroup_id/smart_class_parameters + assign_ansible_roles + /api/hostgroups/:hostgroup_id/assign_ansible_roles + ansible_roles + /api/hostgroups/:hostgroup_id/ansible_roles Otherwise, call ``super``. """ if which in ( + 'assign_ansible_roles', + 'ansible_roles', 'clone', 'puppetclass_ids', 'rebuild_config', @@ -3702,6 +3708,88 @@ def rebuild_config(self, synchronous=True, timeout=None, **kwargs): response = client.put(self.path('rebuild_config'), **kwargs) return _handle_response(response, self._server_config, synchronous, timeout) + def assign_ansible_roles(self, synchronous=True, timeout=None, **kwargs): + """Add an Ansible Role to a hostgroup + + Here is an example of how to use this method:: + hostgroup.assign_ansible_roles(data={'ansible_role_ids': + [ansible_role_id1, ansible_role_id2]}) + + :param synchronous: What should happen if the server returns an HTTP + 202 (accepted) status code? Wait for the task to complete if + ``True``. Immediately return the server's response otherwise. + :param timeout: Maximum number of seconds to wait until timing out. + Defaults to ``nailgun.entity_mixins.TASK_TIMEOUT``. + :param kwargs: Arguments to pass to requests. + :returns: The server's response, with all JSON decoded. + :raises: ``requests.exceptions.HTTPError`` If the server responds with + an HTTP 4XX or 5XX message. + + """ + kwargs = kwargs.copy() + kwargs.update(self._server_config.get_client_kwargs()) + response = client.post(self.path('assign_ansible_roles'), **kwargs) + return _handle_response(response, self._server_config, synchronous, timeout) + + def list_ansible_roles(self, synchronous=True, timeout=None, **kwargs): + """List all Ansible Roles assigned to a hostgroup + + :param synchronous: What should happen if the server returns an HTTP + 202 (accepted) status code? Wait for the task to complete if + ``True``. Immediately return the server's response otherwise. + :param timeout: Maximum number of seconds to wait until timing out. + Defaults to ``nailgun.entity_mixins.TASK_TIMEOUT``. + :param kwargs: Arguments to pass to requests. + :returns: The server's response, with all JSON decoded. + :raises: ``requests.exceptions.HTTPError`` If the server responds with + an HTTP 4XX or 5XX message. + + """ + kwargs = kwargs.copy() + kwargs.update(self._server_config.get_client_kwargs()) + response = client.get(self.path('ansible_roles'), **kwargs) + return _handle_response(response, self._server_config, synchronous, timeout) + + def add_ansible_role(self, synchronous=True, timeout=None, **kwargs): + """Add single Ansible Role to a hostgroup + + :param synchronous: What should happen if the server returns an HTTP + 202 (accepted) status code? Wait for the task to complete if + ``True``. Immediately return the server's response otherwise. + :param timeout: Maximum number of seconds to wait until timing out. + Defaults to ``nailgun.entity_mixins.TASK_TIMEOUT``. + :param kwargs: Arguments to pass to requests. + :returns: The server's response, with all JSON decoded. + :raises: ``requests.exceptions.HTTPError`` If the server responds with + an HTTP 4XX or 5XX message. + """ + kwargs = kwargs.copy() + kwargs.update(self._server_config.get_client_kwargs()) + path = f'{self.path("ansible_roles")}/{kwargs["data"].pop("ansible_role_id")}' + return _handle_response( + client.put(path, **kwargs), self._server_config, synchronous, timeout + ) + + def remove_ansible_role(self, synchronous=True, timeout=None, **kwargs): + """Remove single Ansible Role assigned to a hostgroup + + :param synchronous: What should happen if the server returns an HTTP + 202 (accepted) status code? Wait for the task to complete if + ``True``. Immediately return the server's response otherwise. + :param timeout: Maximum number of seconds to wait until timing out. + Defaults to ``nailgun.entity_mixins.TASK_TIMEOUT``. + :param kwargs: Arguments to pass to requests. + :returns: The server's response, with all JSON decoded. + :raises: ``requests.exceptions.HTTPError`` If the server responds with + an HTTP 4XX or 5XX message. + """ + kwargs = kwargs.copy() + kwargs.update(self._server_config.get_client_kwargs()) + path = f'{self.path("ansible_roles")}/{kwargs["data"].pop("ansible_role_id")}' + return _handle_response( + client.delete(path, **kwargs), self._server_config, synchronous, timeout + ) + class HostPackage(Entity): """A representation of a Host Package entity.""" @@ -4727,6 +4815,46 @@ def list_ansible_roles(self, synchronous=True, timeout=None, **kwargs): response = client.get(self.path('ansible_roles'), **kwargs) return _handle_response(response, self._server_config, synchronous, timeout) + def add_ansible_role(self, synchronous=True, timeout=None, **kwargs): + """Add single Ansible Role to a host + + :param synchronous: What should happen if the server returns an HTTP + 202 (accepted) status code? Wait for the task to complete if + ``True``. Immediately return the server's response otherwise. + :param timeout: Maximum number of seconds to wait until timing out. + Defaults to ``nailgun.entity_mixins.TASK_TIMEOUT``. + :param kwargs: Arguments to pass to requests. + :returns: The server's response, with all JSON decoded. + :raises: ``requests.exceptions.HTTPError`` If the server responds with + an HTTP 4XX or 5XX message. + """ + kwargs = kwargs.copy() + kwargs.update(self._server_config.get_client_kwargs()) + path = f'{self.path("ansible_roles")}/{kwargs["data"].pop("ansible_role_id")}' + return _handle_response( + client.put(path, **kwargs), self._server_config, synchronous, timeout + ) + + def remove_ansible_role(self, synchronous=True, timeout=None, **kwargs): + """Remove single Ansible Role assigned to a host + + :param synchronous: What should happen if the server returns an HTTP + 202 (accepted) status code? Wait for the task to complete if + ``True``. Immediately return the server's response otherwise. + :param timeout: Maximum number of seconds to wait until timing out. + Defaults to ``nailgun.entity_mixins.TASK_TIMEOUT``. + :param kwargs: Arguments to pass to requests. + :returns: The server's response, with all JSON decoded. + :raises: ``requests.exceptions.HTTPError`` If the server responds with + an HTTP 4XX or 5XX message. + """ + kwargs = kwargs.copy() + kwargs.update(self._server_config.get_client_kwargs()) + path = f'{self.path("ansible_roles")}/{kwargs["data"].pop("ansible_role_id")}' + return _handle_response( + client.delete(path, **kwargs), self._server_config, synchronous, timeout + ) + def list_provisioning_templates(self, synchronous=True, timeout=None, **kwargs): """List all Provisioning templates assigned to a Host @@ -8520,7 +8648,14 @@ def sync(self, synchronous=True, timeout=None, **kwargs): return _handle_response(response, self._server_config, synchronous, timeout) -class AnsibleRoles(Entity): +class AnsibleRoles( + Entity, + EntityCreateMixin, + EntityDeleteMixin, + EntityReadMixin, + EntitySearchMixin, + EntityUpdateMixin, +): """A representation of Ansible Roles entity.""" def __init__(self, server_config=None, **kwargs): diff --git a/tests/test_entities.py b/tests/test_entities.py index 5e1a11b2..456d2fe1 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -329,6 +329,8 @@ def test_id_and_which(self): (entities.Host, 'smart_class_parameters'), (entities.Host, 'ansible_roles'), (entities.Host, 'assign_ansible_roles'), + (entities.HostGroup, 'ansible_roles'), + (entities.HostGroup, 'assign_ansible_roles'), (entities.HostGroup, 'clone'), (entities.HostGroup, 'puppetclass_ids'), (entities.HostGroup, 'rebuild_config'), @@ -2158,11 +2160,13 @@ def setUpClass(cls): (entities.ForemanTask(cfg).summary, 'get'), (entities.Organization(**generic).download_debug_certificate, 'get'), (entities.Host(**generic).add_puppetclass, 'post'), + (entities.Host(**generic).assign_ansible_roles, 'post'), (entities.Host(**generic).enc, 'get'), (entities.Host(**generic).errata, 'get'), (entities.Host(**generic).errata_apply, 'put'), (entities.Host(**generic).get_facts, 'get'), (entities.Host(**generic).install_content, 'put'), + (entities.Host(**generic).list_ansible_roles, 'get'), (entities.Host(**generic).list_scparams, 'get'), (entities.Host(**generic).module_streams, 'get'), (entities.Host(**generic).packages, 'get'), @@ -2174,7 +2178,9 @@ def setUpClass(cls): (entities.Host(**generic).bulk_traces, 'post'), (entities.Host(**generic).bulk_resolve_traces, 'put'), (entities.HostGroup(**generic).add_puppetclass, 'post'), + (entities.HostGroup(**generic).assign_ansible_roles, 'post'), (entities.HostGroup(**generic).clone, 'post'), + (entities.HostGroup(**generic).list_ansible_roles, 'get'), (entities.HostGroup(**generic).list_scparams, 'get'), (entities.HostSubscription(**hostsubscription).add_subscriptions, 'put'), (entities.HostSubscription(**hostsubscription).remove_subscriptions, 'put'), @@ -2899,7 +2905,7 @@ def test_read(self): self.read_json_pacther.stop() self.read_pacther.stop() - def test_delete_puppetclass(self): + def test_add_func_with_id(self): """Check that helper method is sane. Assert that: @@ -2907,25 +2913,56 @@ def test_delete_puppetclass(self): * Method has a correct signature. * Method calls `client.*` once. * Method passes the right arguments to `client.*` and special - argument 'puppetclass_id' removed from data dict. + argument 'ansible_role_id' removed from data dict. * Method calls `entities._handle_response` once. * The result of `_handle_response(…)` is the return value. + """ + entity = self.entity + entity.id = 1 + func_param_dict = {entity.add_ansible_role: 'ansible_role_id'} + for func in func_param_dict.keys(): + self.assertEqual(inspect.getfullargspec(func), EXPECTED_ARGSPEC) + kwargs = {'kwarg': gen_integer(), 'data': {func_param_dict[func]: gen_integer()}} + with mock.patch.object(entities, '_handle_response') as handlr: + with mock.patch.object(client, 'put') as client_request: + response = func(**kwargs) + self.assertEqual(client_request.call_count, 1) + self.assertEqual(len(client_request.call_args[0]), 1) + self.assertNotIn(func_param_dict[func], client_request.call_args[1]['data']) + self.assertEqual(client_request.call_args[1], kwargs) + self.assertEqual(handlr.call_count, 1) + self.assertEqual(handlr.return_value, response) + + def test_delete_func_with_id(self): + """Check that helper method is sane. + Assert that: + * Method has a correct signature. + * Method calls `client.*` once. + * Method passes the right arguments to `client.*` and special + argument 'puppetclass_id/ansible_role_id' removed from data dict. + * Method calls `entities._handle_response` once. + * The result of `_handle_response(…)` is the return value. """ entity = self.entity entity.id = 1 - self.assertEqual(inspect.getfullargspec(entity.delete_puppetclass), EXPECTED_ARGSPEC) - kwargs = {'kwarg': gen_integer(), 'data': {'puppetclass_id': gen_integer()}} - with mock.patch.object(entities, '_handle_response') as handlr: - with mock.patch.object(client, 'delete') as client_request: - response = entity.delete_puppetclass(**kwargs) - self.assertEqual(client_request.call_count, 1) - self.assertEqual(len(client_request.call_args[0]), 1) - self.assertNotIn('puppetclass_id', client_request.call_args[1]['data']) - self.assertEqual(client_request.call_args[1], kwargs) - self.assertEqual(handlr.call_count, 1) - self.assertEqual(handlr.return_value, response) + func_param_dict = { + entity.delete_puppetclass: 'puppetclass_id', + entity.remove_ansible_role: 'ansible_role_id', + } + for func in func_param_dict.keys(): + self.assertEqual(inspect.getfullargspec(func), EXPECTED_ARGSPEC) + kwargs = {'kwarg': gen_integer(), 'data': {func_param_dict[func]: gen_integer()}} + with mock.patch.object(entities, '_handle_response') as handlr: + with mock.patch.object(client, 'delete') as client_request: + response = func(**kwargs) + self.assertEqual(client_request.call_count, 1) + self.assertEqual(len(client_request.call_args[0]), 1) + self.assertNotIn(func_param_dict[func], client_request.call_args[1]['data']) + self.assertEqual(client_request.call_args[1], kwargs) + self.assertEqual(handlr.call_count, 1) + self.assertEqual(handlr.return_value, response) def test_clone_hostgroup(self): """Test for :meth:`nailgun.entities.HostGroup.clone` @@ -3051,7 +3088,7 @@ def test_no_facet_attributes(self): self.assertNotIn('content_facet_attributes', read.call_args[0][1]) self.assertIn('content_facet_attributes', read.call_args[0][2]) - def test_delete_puppetclass(self): + def test_add_func_with_id(self): """Check that helper method is sane. Assert that: @@ -3059,23 +3096,54 @@ def test_delete_puppetclass(self): * Method has a correct signature. * Method calls `client.*` once. * Method passes the right arguments to `client.*` and special - argument 'puppetclass_id' removed from data dict. + argument 'ansible_role_id' removed from data dict. * Method calls `entities._handle_response` once. * The result of `_handle_response(…)` is the return value. + """ + entity = entities.Host(self.cfg, id=1) + func_param_dict = {entity.add_ansible_role: 'ansible_role_id'} + for func in func_param_dict.keys(): + self.assertEqual(inspect.getfullargspec(func), EXPECTED_ARGSPEC) + kwargs = {'kwarg': gen_integer(), 'data': {func_param_dict[func]: gen_integer()}} + with mock.patch.object(entities, '_handle_response') as handlr: + with mock.patch.object(client, 'put') as client_request: + response = func(**kwargs) + self.assertEqual(client_request.call_count, 1) + self.assertEqual(len(client_request.call_args[0]), 1) + self.assertNotIn(func_param_dict[func], client_request.call_args[1]['data']) + self.assertEqual(client_request.call_args[1], kwargs) + self.assertEqual(handlr.call_count, 1) + self.assertEqual(handlr.return_value, response) + def test_delete_func_with_id(self): + """Check that helper method is sane. + + Assert that: + + * Method has a correct signature. + * Method calls `client.*` once. + * Method passes the right arguments to `client.*` and special + argument 'puppetclass_id/ansible_role_id' removed from data dict. + * Method calls `entities._handle_response` once. + * The result of `_handle_response(…)` is the return value. """ entity = entities.Host(self.cfg, id=1) - self.assertEqual(inspect.getfullargspec(entity.delete_puppetclass), EXPECTED_ARGSPEC) - kwargs = {'kwarg': gen_integer(), 'data': {'puppetclass_id': gen_integer()}} - with mock.patch.object(entities, '_handle_response') as handlr: - with mock.patch.object(client, 'delete') as client_request: - response = entity.delete_puppetclass(**kwargs) - self.assertEqual(client_request.call_count, 1) - self.assertEqual(len(client_request.call_args[0]), 1) - self.assertNotIn('puppetclass_id', client_request.call_args[1]['data']) - self.assertEqual(client_request.call_args[1], kwargs) - self.assertEqual(handlr.call_count, 1) - self.assertEqual(handlr.return_value, response) + func_param_dict = { + entity.delete_puppetclass: 'puppetclass_id', + entity.remove_ansible_role: 'ansible_role_id', + } + for func in func_param_dict.keys(): + self.assertEqual(inspect.getfullargspec(func), EXPECTED_ARGSPEC) + kwargs = {'kwarg': gen_integer(), 'data': {func_param_dict[func]: gen_integer()}} + with mock.patch.object(entities, '_handle_response') as handlr: + with mock.patch.object(client, 'delete') as client_request: + response = func(**kwargs) + self.assertEqual(client_request.call_count, 1) + self.assertEqual(len(client_request.call_args[0]), 1) + self.assertNotIn(func_param_dict[func], client_request.call_args[1]['data']) + self.assertEqual(client_request.call_args[1], kwargs) + self.assertEqual(handlr.call_count, 1) + self.assertEqual(handlr.return_value, response) def test_disassociate(self): """Disassociate host"""