From a7ff73355731d597467b15d7f4079a6eeb1f45dc Mon Sep 17 00:00:00 2001 From: Avi Layani Date: Wed, 13 Dec 2023 18:28:31 +0200 Subject: [PATCH] Adding hosts bulk deletion feature (#14462) * Adding hosts bulk deletion feature Signed-off-by: Avi Layani * fix the type of the argument Signed-off-by: Avi Layani * fixing activity_entry tracking Signed-off-by: Avi Layani * Revert "fixing activity_entry tracking" This reverts commit c8eab52c2ccc5abe215d56d1704ba1157e5fbbd0. Since the bulk_delete is not related to an inventory, only hosts which can be from different inventories. * get only needed vars to reduce memory consumption Signed-off-by: Avi Layani * filtering the data to reduce memory increase the number of queries Signed-off-by: Avi Layani * update the activity stream for inventories Signed-off-by: Avi Layani * fix the changes dict initialiazation Signed-off-by: Avi Layani --------- Signed-off-by: Avi Layani --- awx/api/serializers.py | 93 ++++++++++++ .../templates/api/bulk_host_delete_view.md | 22 +++ awx/api/urls/urls.py | 2 + awx/api/views/bulk.py | 18 +++ awx/main/conf.py | 10 ++ awx/main/tests/functional/test_bulk.py | 136 ++++++++++++++++++ awx/settings/defaults.py | 3 + awx_collection/meta/runtime.yml | 1 + .../plugins/modules/bulk_host_delete.py | 65 +++++++++ awx_collection/test/awx/test_bulk.py | 25 ++++ awx_collection/test/awx/test_completeness.py | 1 + .../targets/bulk_host_delete/tasks/main.yml | 80 +++++++++++ awxkit/awxkit/cli/custom.py | 20 +++ awxkit/awxkit/cli/options.py | 4 + docs/bulk_api.md | 18 +++ 15 files changed, 498 insertions(+) create mode 100644 awx/api/templates/api/bulk_host_delete_view.md create mode 100644 awx_collection/plugins/modules/bulk_host_delete.py create mode 100644 awx_collection/tests/integration/targets/bulk_host_delete/tasks/main.yml diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 392a068d06de..46875297d7bd 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2201,6 +2201,99 @@ def create(self, validated_data): return return_data +class BulkHostDeleteSerializer(serializers.Serializer): + hosts = serializers.ListField( + allow_empty=False, + max_length=100000, + write_only=True, + help_text=_('List of hosts ids to be deleted, e.g. [105, 130, 131, 200]'), + ) + + class Meta: + model = Host + fields = ('hosts',) + + def validate(self, attrs): + request = self.context.get('request', None) + max_hosts = settings.BULK_HOST_MAX_DELETE + # Validating the number of hosts to be deleted + if len(attrs['hosts']) > max_hosts: + raise serializers.ValidationError( + { + "ERROR": 'Number of hosts exceeds system setting BULK_HOST_MAX_DELETE', + "BULK_HOST_MAX_DELETE": max_hosts, + "Hosts_count": len(attrs['hosts']), + } + ) + + # Getting list of all host objects, filtered by the list of the hosts to delete + attrs['host_qs'] = Host.objects.get_queryset().filter(pk__in=attrs['hosts']).only('id', 'inventory_id', 'name') + + # Converting the queryset data in a dict. to reduce the number of queries when + # manipulating the data + attrs['hosts_data'] = attrs['host_qs'].values() + + if len(attrs['host_qs']) == 0: + error_hosts = {host: "Hosts do not exist or you lack permission to delete it" for host in attrs['hosts']} + raise serializers.ValidationError({'hosts': error_hosts}) + + if len(attrs['host_qs']) < len(attrs['hosts']): + hosts_exists = [host['id'] for host in attrs['hosts_data']] + failed_hosts = list(set(attrs['hosts']).difference(hosts_exists)) + error_hosts = {host: "Hosts do not exist or you lack permission to delete it" for host in failed_hosts} + raise serializers.ValidationError({'hosts': error_hosts}) + + # Getting all inventories that the hosts can be in + inv_list = list(set([host['inventory_id'] for host in attrs['hosts_data']])) + + # Checking that the user have permission to all inventories + errors = dict() + for inv in Inventory.objects.get_queryset().filter(pk__in=inv_list): + if request and not request.user.is_superuser: + if request.user not in inv.admin_role: + errors[inv.name] = "Lack permissions to delete hosts from this inventory." + if errors != {}: + raise PermissionDenied({"inventories": errors}) + + # check the inventory type only if the user have permission to it. + errors = dict() + for inv in Inventory.objects.get_queryset().filter(pk__in=inv_list): + if inv.kind != '': + errors[inv.name] = "Hosts can only be deleted from manual inventories." + if errors != {}: + raise serializers.ValidationError({"inventories": errors}) + attrs['inventories'] = inv_list + return attrs + + def delete(self, validated_data): + result = {"hosts": dict()} + changes = {'deleted_hosts': dict()} + for inventory in validated_data['inventories']: + changes['deleted_hosts'][inventory] = list() + + for host in validated_data['hosts_data']: + result["hosts"][host["id"]] = f"The host {host['name']} was deleted" + changes['deleted_hosts'][host["inventory_id"]].append({"host_id": host["id"], "host_name": host["name"]}) + + try: + validated_data['host_qs'].delete() + except Exception as e: + raise serializers.ValidationError({"detail": _(f"cannot delete hosts, host deletion error {e}")}) + + request = self.context.get('request', None) + + for inventory in validated_data['inventories']: + activity_entry = ActivityStream.objects.create( + operation='update', + object1='inventory', + changes=json.dumps(changes['deleted_hosts'][inventory]), + actor=request.user, + ) + activity_entry.inventory.add(inventory) + + return result + + class GroupTreeSerializer(GroupSerializer): children = serializers.SerializerMethodField() diff --git a/awx/api/templates/api/bulk_host_delete_view.md b/awx/api/templates/api/bulk_host_delete_view.md new file mode 100644 index 000000000000..1fff2a7e3c91 --- /dev/null +++ b/awx/api/templates/api/bulk_host_delete_view.md @@ -0,0 +1,22 @@ +# Bulk Host Delete + +This endpoint allows the client to delete multiple hosts from inventories. +They may do this by providing a list of hosts ID's to be deleted. + +Example: + + { + "hosts": [1, 2, 3, 4, 5] + } + +Return data: + + { + "hosts": { + "1": "The host a1 was deleted", + "2": "The host a2 was deleted", + "3": "The host a3 was deleted", + "4": "The host a4 was deleted", + "5": "The host a5 was deleted", + } + } \ No newline at end of file diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index c74f9f97e6c7..030ba25edef5 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -36,6 +36,7 @@ from awx.api.views.bulk import ( BulkView, BulkHostCreateView, + BulkHostDeleteView, BulkJobLaunchView, ) @@ -152,6 +153,7 @@ re_path(r'^workflow_approvals/', include(workflow_approval_urls)), re_path(r'^bulk/$', BulkView.as_view(), name='bulk'), re_path(r'^bulk/host_create/$', BulkHostCreateView.as_view(), name='bulk_host_create'), + re_path(r'^bulk/host_delete/$', BulkHostDeleteView.as_view(), name='bulk_host_delete'), re_path(r'^bulk/job_launch/$', BulkJobLaunchView.as_view(), name='bulk_job_launch'), ] diff --git a/awx/api/views/bulk.py b/awx/api/views/bulk.py index f8d52354ceea..a78dc43a3735 100644 --- a/awx/api/views/bulk.py +++ b/awx/api/views/bulk.py @@ -34,6 +34,7 @@ def get(self, request, format=None): '''List top level resources''' data = OrderedDict() data['host_create'] = reverse('api:bulk_host_create', request=request) + data['host_delete'] = reverse('api:bulk_host_delete', request=request) data['job_launch'] = reverse('api:bulk_job_launch', request=request) return Response(data) @@ -72,3 +73,20 @@ def post(self, request): result = serializer.create(serializer.validated_data) return Response(result, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class BulkHostDeleteView(GenericAPIView): + permission_classes = [IsAuthenticated] + model = Host + serializer_class = serializers.BulkHostDeleteSerializer + allowed_methods = ['GET', 'POST', 'OPTIONS'] + + def get(self, request): + return Response({"detail": "Bulk delete hosts with this endpoint"}, status=status.HTTP_200_OK) + + def post(self, request): + serializer = serializers.BulkHostDeleteSerializer(data=request.data, context={'request': request}) + if serializer.is_valid(): + result = serializer.delete(serializer.validated_data) + return Response(result, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/awx/main/conf.py b/awx/main/conf.py index e11bb15ae456..e50aac8caf7e 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -827,6 +827,16 @@ category_slug='bulk', ) +register( + 'BULK_HOST_MAX_DELETE', + field_class=fields.IntegerField, + default=250, + label=_('Max number of hosts to allow to be deleted in a single bulk action'), + help_text=_('Max number of hosts to allow to be deleted in a single bulk action'), + category=_('Bulk Actions'), + category_slug='bulk', +) + register( 'UI_NEXT', field_class=fields.BooleanField, diff --git a/awx/main/tests/functional/test_bulk.py b/awx/main/tests/functional/test_bulk.py index d05bb7a1f8a2..6b166cdf2bff 100644 --- a/awx/main/tests/functional/test_bulk.py +++ b/awx/main/tests/functional/test_bulk.py @@ -309,3 +309,139 @@ def test_bulk_job_set_all_prompt(job_template, organization, inventory, project, assert node[0].limit == 'kansas' assert node[0].skip_tags == 'foobar' assert node[0].job_tags == 'untagged' + + +@pytest.mark.django_db +@pytest.mark.parametrize('num_hosts, num_queries', [(1, 70), (10, 150), (25, 250)]) +def test_bulk_host_delete_num_queries(organization, inventory, post, get, user, num_hosts, num_queries, django_assert_max_num_queries): + ''' + If I am a... + org admin + inventory admin at org level + admin of a particular inventory + superuser + + Bulk Host delete should take under a certain number of queries + ''' + users_list = setup_admin_users_list(organization, inventory, user) + for u in users_list: + hosts = [{'name': str(uuid4())} for i in range(num_hosts)] + with django_assert_max_num_queries(num_queries): + bulk_host_create_response = post(reverse('api:bulk_host_create'), {'inventory': inventory.id, 'hosts': hosts}, u, expect=201).data + assert len(bulk_host_create_response['hosts']) == len(hosts), f"unexpected number of hosts created for user {u}" + hosts_ids_created = get_inventory_hosts(get, inventory.id, u) + bulk_host_delete_response = post(reverse('api:bulk_host_delete'), {'hosts': hosts_ids_created}, u, expect=201).data + assert len(bulk_host_delete_response['hosts'].keys()) == len(hosts), f"unexpected number of hosts deleted for user {u}" + + +@pytest.mark.django_db +def test_bulk_host_delete_rbac(organization, inventory, post, get, user): + ''' + If I am a... + org admin + inventory admin at org level + admin of a particular invenotry + ... I can bulk delete hosts + + Everyone else cannot + ''' + admin_users_list = setup_admin_users_list(organization, inventory, user) + users_list = setup_none_admin_uses_list(organization, inventory, user) + + for indx, u in enumerate(admin_users_list): + bulk_host_create_response = post( + reverse('api:bulk_host_create'), {'inventory': inventory.id, 'hosts': [{'name': f'foobar-{indx}'}]}, u, expect=201 + ).data + assert len(bulk_host_create_response['hosts']) == 1, f"unexpected number of hosts created for user {u}" + assert Host.objects.filter(inventory__id=inventory.id)[0].name == f'foobar-{indx}' + hosts_ids_created = get_inventory_hosts(get, inventory.id, u) + bulk_host_delete_response = post(reverse('api:bulk_host_delete'), {'hosts': hosts_ids_created}, u, expect=201).data + assert len(bulk_host_delete_response['hosts'].keys()) == 1, f"unexpected number of hosts deleted by user {u}" + + for indx, create_u in enumerate(admin_users_list): + bulk_host_create_response = post( + reverse('api:bulk_host_create'), {'inventory': inventory.id, 'hosts': [{'name': f'foobar2-{indx}'}]}, create_u, expect=201 + ).data + print(bulk_host_create_response) + assert bulk_host_create_response['hosts'][0]['name'] == f'foobar2-{indx}' + hosts_ids_created = get_inventory_hosts(get, inventory.id, create_u) + print(f"Try to delete {hosts_ids_created}") + for delete_u in users_list: + bulk_host_delete_response = post(reverse('api:bulk_host_delete'), {'hosts': hosts_ids_created}, delete_u, expect=403).data + assert "Lack permissions to delete hosts from this inventory." in bulk_host_delete_response['inventories'].values() + + +@pytest.mark.django_db +def test_bulk_host_delete_from_multiple_inv(organization, inventory, post, get, user): + ''' + If I am inventory admin at org level + + Bulk Host delete should be enabled only on my inventory + ''' + num_hosts = 10 + inventory.organization = organization + + # Create second inventory + inv2 = organization.inventories.create(name="second-test-inv") + inv2.organization = organization + admin2_user = user('inventory2_admin', False) + inv2.admin_role.members.add(admin2_user) + + admin_user = user('inventory_admin', False) + inventory.admin_role.members.add(admin_user) + + organization.member_role.members.add(admin_user) + organization.member_role.members.add(admin2_user) + + hosts = [{'name': str(uuid4())} for i in range(num_hosts)] + hosts2 = [{'name': str(uuid4())} for i in range(num_hosts)] + + # create hosts in each of the inventories + bulk_host_create_response = post(reverse('api:bulk_host_create'), {'inventory': inventory.id, 'hosts': hosts}, admin_user, expect=201).data + assert len(bulk_host_create_response['hosts']) == len(hosts), f"unexpected number of hosts created for user {admin_user}" + + bulk_host_create_response2 = post(reverse('api:bulk_host_create'), {'inventory': inv2.id, 'hosts': hosts2}, admin2_user, expect=201).data + assert len(bulk_host_create_response2['hosts']) == len(hosts), f"unexpected number of hosts created for user {admin2_user}" + + # get all hosts ids - from both inventories + hosts_ids_created = get_inventory_hosts(get, inventory.id, admin_user) + hosts_ids_created += get_inventory_hosts(get, inv2.id, admin2_user) + + expected_error = "Lack permissions to delete hosts from this inventory." + # try to delete ALL hosts with admin user of inventory 1. + for inv_name, invadmin in zip([inv2.name, inventory.name], [admin_user, admin2_user]): + bulk_host_delete_response = post(reverse('api:bulk_host_delete'), {'hosts': hosts_ids_created}, invadmin, expect=403).data + result_message = bulk_host_delete_response['inventories'][inv_name] + assert result_message == expected_error, f"deleted hosts without permission by user {invadmin}" + + +def setup_admin_users_list(organization, inventory, user): + inventory.organization = organization + inventory_admin = user('inventory_admin', False) + org_admin = user('org_admin', False) + org_inv_admin = user('org_admin', False) + superuser = user('admin', True) + for u in [org_admin, org_inv_admin, inventory_admin]: + organization.member_role.members.add(u) + organization.admin_role.members.add(org_admin) + organization.inventory_admin_role.members.add(org_inv_admin) + inventory.admin_role.members.add(inventory_admin) + return [inventory_admin, org_inv_admin, superuser, org_admin] + + +def setup_none_admin_uses_list(organization, inventory, user): + inventory.organization = organization + auditor = user('auditor', False) + member = user('member', False) + use_inv_member = user('member', False) + for u in [auditor, member, use_inv_member]: + organization.member_role.members.add(u) + inventory.use_role.members.add(use_inv_member) + organization.auditor_role.members.add(auditor) + return [auditor, member, use_inv_member] + + +def get_inventory_hosts(get, inv_id, use_user): + data = get(reverse('api:inventory_hosts_list', kwargs={'pk': inv_id}), use_user, expect=200).data + results = [host['id'] for host in data['results']] + return results diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 0b5e000b40b0..8bdb9617ac3a 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -131,6 +131,9 @@ # Maximum number of host that can be created in 1 bulk host create BULK_HOST_MAX_CREATE = 100 +# Maximum number of host that can be deleted in 1 bulk host delete +BULK_HOST_MAX_DELETE = 250 + SITE_ID = 1 # Make this unique, and don't share it with anybody. diff --git a/awx_collection/meta/runtime.yml b/awx_collection/meta/runtime.yml index 3e50a52e69ee..18fa4b592e2d 100644 --- a/awx_collection/meta/runtime.yml +++ b/awx_collection/meta/runtime.yml @@ -8,6 +8,7 @@ action_groups: - application - bulk_job_launch - bulk_host_create + - bulk_host_delete - controller_meta - credential_input_source - credential diff --git a/awx_collection/plugins/modules/bulk_host_delete.py b/awx_collection/plugins/modules/bulk_host_delete.py new file mode 100644 index 000000000000..12468e6028ff --- /dev/null +++ b/awx_collection/plugins/modules/bulk_host_delete.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: bulk_host_delete +author: "Avi Layani (@Avilir)" +short_description: Bulk host delete in Automation Platform Controller +description: + - Single-request bulk host deletion in Automation Platform Controller. + - Provides a way to delete many hosts at once from inventories in Controller. +options: + hosts: + description: + - List of hosts id's to delete from inventory. + required: True + type: list + elements: int +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Bulk host delete + bulk_host_delete: + hosts: + - 1 + - 2 +''' + +from ..module_utils.controller_api import ControllerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + hosts=dict(required=True, type='list', elements='int'), + ) + + # Create a module for ourselves + module = ControllerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + hosts = module.params.get('hosts') + + # Delete the hosts + result = module.post_endpoint("bulk/host_delete", data={"hosts": hosts}) + + if result['status_code'] != 201: + module.fail_json(msg="Failed to delete hosts, see response for details", response=result) + + module.json_output['changed'] = True + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/test/awx/test_bulk.py b/awx_collection/test/awx/test_bulk.py index e0315732dbd1..da45621dd0cf 100644 --- a/awx_collection/test/awx/test_bulk.py +++ b/awx_collection/test/awx/test_bulk.py @@ -45,3 +45,28 @@ def test_bulk_host_create(run_module, admin_user, inventory): resp_hosts = inventory.hosts.all().values_list('name', flat=True) for h in hosts: assert h['name'] in resp_hosts + + +@pytest.mark.django_db +def test_bulk_host_delete(run_module, admin_user, inventory): + hosts = [dict(name="127.0.0.1"), dict(name="foo.dns.org")] + result = run_module( + 'bulk_host_create', + { + 'inventory': inventory.name, + 'hosts': hosts, + }, + admin_user, + ) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + resp_hosts_ids = list(inventory.hosts.all().values_list('id', flat=True)) + result = run_module( + 'bulk_host_delete', + { + 'hosts': resp_hosts_ids, + }, + admin_user, + ) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result diff --git a/awx_collection/test/awx/test_completeness.py b/awx_collection/test/awx/test_completeness.py index b4eb0ad3e4f9..b3c6e6e27f23 100644 --- a/awx_collection/test/awx/test_completeness.py +++ b/awx_collection/test/awx/test_completeness.py @@ -50,6 +50,7 @@ extra_endpoints = { 'bulk_job_launch': '/api/v2/bulk/job_launch/', 'bulk_host_create': '/api/v2/bulk/host_create/', + 'bulk_host_delete': '/api/v2/bulk/host_delete/', } # Global module parameters we can ignore diff --git a/awx_collection/tests/integration/targets/bulk_host_delete/tasks/main.yml b/awx_collection/tests/integration/targets/bulk_host_delete/tasks/main.yml new file mode 100644 index 000000000000..5f38efe7c6b2 --- /dev/null +++ b/awx_collection/tests/integration/targets/bulk_host_delete/tasks/main.yml @@ -0,0 +1,80 @@ +--- +- name: "Generate a random string for test" + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: "test_id is not defined" + +- name: "Generate a unique name" + set_fact: + bulk_inv_name: "AWX-Collection-tests-bulk_host_create-{{ test_id }}" + +- name: "Get our collection package" + controller_meta: + register: "controller_meta" + +- name: "Generate the name of our plugin" + set_fact: + plugin_name: "{{ controller_meta.prefix }}.controller_api" + +- name: "Create an inventory" + inventory: + name: "{{ bulk_inv_name }}" + organization: "Default" + state: "present" + register: "inventory_result" + +- name: "Bulk Host Create" + bulk_host_create: + hosts: + - name: "123.456.789.123" + description: "myhost1" + variables: + food: "carrot" + color: "orange" + - name: "example.dns.gg" + description: "myhost2" + enabled: "false" + inventory: "{{ bulk_inv_name }}" + register: "result" + +- assert: + that: + - "result is not failed" + +- name: "Get our collection package" + controller_meta: + register: "controller_meta" + +- name: "Generate the name of our plugin" + set_fact: + plugin_name: "{{ controller_meta.prefix }}.controller_api" + +- name: "Setting the inventory hosts endpoint" + set_fact: + endpoint: "inventories/{{ inventory_result.id }}/hosts/" + +- name: "Get hosts information from inventory" + set_fact: + hosts_created: "{{ query(plugin_name, endpoint, return_objects=True) }}" + host_id_list: [] + +- name: "Extract host IDs from hosts information" + set_fact: + host_id_list: "{{ host_id_list + [item.id] }}" + loop: "{{ hosts_created }}" + +- name: "Bulk Host Delete" + bulk_host_delete: + hosts: "{{ host_id_list }}" + register: "result" + +- assert: + that: + - "result is not failed" + +# cleanup +- name: "Delete inventory" + inventory: + name: "{{ bulk_inv_name }}" + organization: "Default" + state: "absent" diff --git a/awxkit/awxkit/cli/custom.py b/awxkit/awxkit/cli/custom.py index 431fef07e8d4..6d2be2475cb3 100644 --- a/awxkit/awxkit/cli/custom.py +++ b/awxkit/awxkit/cli/custom.py @@ -143,6 +143,26 @@ def perform(self, **kwargs): return response +class BulkHostDelete(CustomAction): + action = 'host_delete' + resource = 'bulk' + + @property + def options_endpoint(self): + return self.page.endpoint + '{}/'.format(self.action) + + def add_arguments(self, parser, resource_options_parser): + options = self.page.connection.options(self.options_endpoint) + if options.ok: + options = options.json()['actions']['POST'] + resource_options_parser.options['HOSTDELETEPOST'] = options + resource_options_parser.build_query_arguments(self.action, 'HOSTDELETEPOST') + + def perform(self, **kwargs): + response = self.page.get().host_delete.post(kwargs) + return response + + class ProjectUpdate(Launchable, CustomAction): action = 'update' resource = 'projects' diff --git a/awxkit/awxkit/cli/options.py b/awxkit/awxkit/cli/options.py index 848e4f6bf7d3..ecedf3918d23 100644 --- a/awxkit/awxkit/cli/options.py +++ b/awxkit/awxkit/cli/options.py @@ -270,6 +270,10 @@ def json_or_yaml(v, expected_type=dict): if k == 'hosts': kwargs['type'] = list_of_json_or_yaml kwargs['required'] = required = True + if method == "host_delete": + if k == 'hosts': + kwargs['type'] = list_of_json_or_yaml + kwargs['required'] = required = True if method == "job_launch": if k == 'jobs': kwargs['type'] = list_of_json_or_yaml diff --git a/docs/bulk_api.md b/docs/bulk_api.md index 493e9575ba65..5fdf94b4e2ea 100644 --- a/docs/bulk_api.md +++ b/docs/bulk_api.md @@ -3,6 +3,7 @@ Bulk API endpoints allows to perform bulk operations in single web request. There are currently following bulk api actions: - /api/v2/bulk/job_launch - /api/v2/bulk/host_create +- /api/v2/bulk/host_delete Making individual API calls in rapid succession or at high concurrency can overwhelm AWX's ability to serve web requests. When the application's ability to serve is exhausted, clients often receive 504 timeout errors. @@ -99,3 +100,20 @@ Following is an example of a post request at the /api/v2/bulk/host_create: The above will add 6 hosts in the inventory. The maximum number of hosts allowed to be added is controlled by the setting `BULK_HOST_MAX_CREATE`. The default is 100 hosts. Additionally, nginx limits the maximum payload size, which is very likely when posting a large number of hosts in one request with variable data associated with them. The maximum payload size is 1MB unless overridden in your nginx config. + + +## Bulk Host Delete + +Provides feature in the API that allows a single web request to delete multiple hosts from an inventory. + +Following is an example of a post request at the /api/v2/bulk/host_delete: + + + { + "hosts": [3, 4, 5, 6, 7 ,8, 9, 10] + } + + +The above will delete 8 hosts from the inventory. + +The maximum number of hosts allowed to be deleted is controlled by the setting `BULK_HOST_MAX_DELETE`. The default is 250 hosts. Additionally, nginx limits the maximum payload size, which is very likely when posting a large number of hosts in one request with variable data associated with them. The maximum payload size is 1MB unless overridden in your nginx config.