diff --git a/cli/README.md b/cli/README.md index 9e92bc3..74b381d 100644 --- a/cli/README.md +++ b/cli/README.md @@ -149,7 +149,8 @@ Manages **Scheduled Repair** in AxonOps. * `--paxosonly` Run paxos repair only. Default is false. * `--skippaxos` Skip paxos repair. Default is false. * `--delete` Delete Scheduled Repair. This option needs to be paired with a tags value to identify which scheduled - repair job to disable. + repair job to delete. +* `--deleteall` Delete all Scheduled Repairs. This removes all scheduled repair jobs from the cluster. #### Examples: @@ -270,4 +271,10 @@ Delete a scheduled repair job with specific tags: ```shell $ pipenv run python axonops.py scheduledrepair --delete --tags 'Weekly repair' +``` + +Delete all scheduled repair jobs: + +```shell +$ pipenv run python axonops.py scheduledrepair --deleteall ``` \ No newline at end of file diff --git a/cli/axonopscli/application.py b/cli/axonopscli/application.py index fed5823..0ad1bbd 100644 --- a/cli/axonopscli/application.py +++ b/cli/axonopscli/application.py @@ -122,6 +122,8 @@ def run(self, argv: Sequence): help='Tag for the repair job', default="") scheduledrepair_parser.add_argument('--delete', action='store_true', help='Delete the scheduled repair instead of enabling it') + scheduledrepair_parser.add_argument('--deleteall', action='store_true', + help='Delete all scheduled repairs') paxos_group = scheduledrepair_parser.add_mutually_exclusive_group() paxos_group.add_argument('--paxosonly', action='store_true', default=False, @@ -207,7 +209,9 @@ def run_scheduled_repair(self, args: argparse.Namespace): scheduled_repair = ScheduledRepair(axonops, args) - scheduled_repair.remove_old_repairs_from_axonops() + if args.deleteall: + scheduled_repair.remove_all_repairs_from_axonops() + return scheduled_repair.set_options() diff --git a/cli/axonopscli/components/scheduled_repair.py b/cli/axonopscli/components/scheduled_repair.py index f5aec6d..699e536 100644 --- a/cli/axonopscli/components/scheduled_repair.py +++ b/cli/axonopscli/components/scheduled_repair.py @@ -15,35 +15,28 @@ def __init__(self, axonops, args): self.full_repair_url = f"{self.repair_url}/{args.org}/cassandra/{args.cluster}" self.full_cassandrascheduledrepair_url = f"{self.cassandrascheduledrepair_url}/{args.org}/cassandra/{args.cluster}" - def remove_old_repairs_from_axonops(self): - """ Check if the scheduled repair already exists in AxonOps, if so, remove it. """ + def remove_all_repairs_from_axonops(self): + """ Remove all scheduled repairs from AxonOps. """ if self.args.v: - print("Checking if scheduled repair already exists") + print("Removing all scheduled repairs") - if self.args.tags != "": + response = self.axonops.do_request( + url=self.full_repair_url, + method='GET', + ) + if not response: if self.args.v: - print("Getting scheduled repair with tag:", self.args.tags) - response = self.axonops.do_request( - url=self.full_repair_url, - method='GET', - ) - if not response: + print("No response received when checking for existing scheduled repair") + return + elif 'ScheduledRepairs' in response and response['ScheduledRepairs']: + for repair in response['ScheduledRepairs']: if self.args.v: - print("No response received when checking for existing scheduled repair") - return - elif 'ScheduledRepairs' in response and response['ScheduledRepairs']: - for repair in response['ScheduledRepairs']: - if self.args.v: - print(f"Checking scheduled repair: {repair['ID']}") - self.remove_repair(repair['ID']) - if 'Params' in repair: - print(repair['Params']) - - else: - print("Repair tag not found, this will be threaded as a new scheduled repair") + print(f"Deleting scheduled repair: {repair['ID']}") + self.remove_repair(repair['ID']) + if 'Params' in repair: + print(repair['Params']) else: - if self.args.v: - print("No tag provided, this will be threaded as a new scheduled repair") + print("No scheduled repairs found") def set_options(self): """Apply optional CLI parameters into the payload before sending it.""" @@ -100,10 +93,35 @@ def set_repair(self): if self.args.v: print("POST", self.full_add_repair_url, self.repair_data) - if self.args.delete: + if self.args.tags != "": + response = self.axonops.do_request( + url=self.full_repair_url, + method='GET', + ) + if not response: + if self.args.v: + print("No response received when checking for existing scheduled repair") + return + elif 'ScheduledRepairs' in response and response['ScheduledRepairs']: + for repair in response['ScheduledRepairs']: + if 'tag' not in repair['Params'][0]: + if self.args.v: + print(f"Scheduled repair {repair['ID']} has no tag, skipping") + continue + if self.args.tags != repair['Params'][0]['tag']: + if self.args.v: + print(f"Scheduled repair {repair['ID']} tag does not match, skipping") + continue + if self.args.v: + print(f"Deleting scheduled repair: {repair['ID']}") + self.remove_repair(repair['ID']) + if 'Params' in repair and self.args.v: + print(repair['Params']) + else: + print("No scheduled repairs found") + else: if self.args.v: - print("This scheduled repair is delete, not sending to AxonOps") - return + print("Tags are always required for deletions") self.axonops.do_request( url=self.full_add_repair_url, diff --git a/tests/scheduled_repairs/run_scheduled_repair_tests.sh b/tests/scheduled_repairs/run_scheduled_repair_tests.sh index b529cae..7e11e31 100755 --- a/tests/scheduled_repairs/run_scheduled_repair_tests.sh +++ b/tests/scheduled_repairs/run_scheduled_repair_tests.sh @@ -11,6 +11,7 @@ DEFAULT_URL="http://localhost:3000" DEFAULT_ORG="testorg" DEFAULT_CLUSTER="testcluster" INTERACTIVE=false +CLEAN=false # Use environment variables or defaults URL="${AXONOPS_URL:-$DEFAULT_URL}" @@ -36,6 +37,10 @@ while [[ $# -gt 0 ]]; do INTERACTIVE=true shift ;; + --clean) + CLEAN=true + shift + ;; --help) echo "Usage: $0 [options]" echo "Options:" @@ -43,6 +48,7 @@ while [[ $# -gt 0 ]]; do echo " --org ORG Organization name (default: $DEFAULT_ORG)" echo " --cluster NAME Cluster name (default: $DEFAULT_CLUSTER)" echo " --confirm, -i Interactive mode - pause before each test for confirmation" + echo " --clean Run Test 0 to delete all scheduled repairs before running tests" echo "" echo "You can also set environment variables:" echo " AXONOPS_URL, AXONOPS_ORG, AXONOPS_CLUSTER" @@ -54,6 +60,7 @@ while [[ $# -gt 0 ]]; do echo " $0 --url https://dash.axonops.cloud # Use SaaS" echo " $0 --org myorg --cluster mycluster # Different org/cluster" echo " $0 --confirm # Interactive mode" + echo " $0 --clean # Clean all repairs before tests" exit 0 ;; *) @@ -84,6 +91,7 @@ run_test() { -e "org=$ORG" \ -e "cluster=$CLUSTER" \ -e "interactive=$INTERACTIVE" \ + -e "clean=$CLEAN" \ -v local exit_code=$? set +x diff --git a/tests/scheduled_repairs/test_scheduled_repair.yml b/tests/scheduled_repairs/test_scheduled_repair.yml index 3832657..18279e9 100644 --- a/tests/scheduled_repairs/test_scheduled_repair.yml +++ b/tests/scheduled_repairs/test_scheduled_repair.yml @@ -12,6 +12,7 @@ axonops_username: "{{ lookup('env', 'AXONOPS_USERNAME') | default('', true) }}" axonops_password: "{{ lookup('env', 'AXONOPS_PASSWORD') | default('', true) }}" interactive_mode: "{{ interactive | default('false') | bool }}" + clean_mode: "{{ clean | default('false') | bool }}" repairs_api_url: "{{ axonops_url }}/api/v1/repair/{{ axonops_org }}/cassandra/{{ axonops_cluster }}" # Build the common auth/connection args (must come before subcommand) common_args: >- @@ -26,6 +27,50 @@ uri_headers: "{{ {'Authorization': 'Bearer ' + axonops_token} if axonops_token else {} }}" tasks: + # =========================================================================== + # Test 0: Clean all scheduled repairs (only when --clean is used) + # =========================================================================== + - name: "Confirm Test 0: Delete all scheduled repairs" + ansible.builtin.pause: + prompt: "Press Enter to run Test 0 (Delete all scheduled repairs) or Ctrl+C to abort" + when: clean_mode | bool and interactive_mode | bool + + - name: "Test 0: Delete all scheduled repairs" + ansible.builtin.shell: | + cd {{ cli_dir }} && pipenv run python axonops.py {{ common_args }} scheduledrepair \ + --deleteall + register: deleteall_result + when: clean_mode | bool + ignore_errors: true + + - name: Display deleteall result + ansible.builtin.debug: + var: deleteall_result + when: clean_mode | bool + + - name: "Verify Test 0: Fetch scheduled repairs from API" + ansible.builtin.uri: + url: "{{ repairs_api_url }}" + method: GET + headers: "{{ uri_headers }}" + user: "{{ axonops_username if not axonops_token and axonops_username else omit }}" + password: "{{ axonops_password if not axonops_token and axonops_password else omit }}" + force_basic_auth: "{{ true if not axonops_token and axonops_username else false }}" + return_content: true + status_code: [200, 201] + register: repairs_response_0 + when: clean_mode | bool and deleteall_result.rc == 0 + + - name: "Verify Test 0: Check all scheduled repairs were deleted" + ansible.builtin.assert: + that: + - (repairs_response_0.json.ScheduledRepairs or []) | length == 0 + fail_msg: "Not all scheduled repairs were deleted" + success_msg: "All scheduled repairs were successfully deleted" + register: verify_result_0 + when: clean_mode | bool and deleteall_result.rc == 0 + ignore_errors: true + # =========================================================================== # Test 1: Basic scheduled repair (run immediately) # =========================================================================== @@ -937,6 +982,85 @@ when: comprehensive_repair_result.rc == 0 and repairs_response_20.json.ScheduledRepairs is defined ignore_errors: true + # =========================================================================== + # Test 21: Delete scheduled repair + # =========================================================================== + - name: "Confirm Test 21: Delete scheduled repair" + ansible.builtin.pause: + prompt: "Press Enter to run Test 21 (Delete scheduled repair) or Ctrl+C to abort" + when: interactive_mode | bool + + - name: "Test 21 - Step 1: Create a scheduled repair to delete" + ansible.builtin.shell: | + cd {{ cli_dir }} && pipenv run python axonops.py {{ common_args }} scheduledrepair \ + --scheduleexpr '0 0 * * 0' \ + --tags 'Test 21' + register: delete_test_create_result + ignore_errors: true + + - name: Display create result for delete test + ansible.builtin.debug: + var: delete_test_create_result + + - name: "Test 21 - Step 2: Verify repair was created" + ansible.builtin.uri: + url: "{{ repairs_api_url }}" + method: GET + headers: "{{ uri_headers }}" + user: "{{ axonops_username if not axonops_token and axonops_username else omit }}" + password: "{{ axonops_password if not axonops_token and axonops_password else omit }}" + force_basic_auth: "{{ true if not axonops_token and axonops_username else false }}" + return_content: true + status_code: [200, 201] + register: repairs_response_21_before + when: delete_test_create_result.rc == 0 + + - name: "Test 21 - Step 2: Assert repair exists before deletion" + ansible.builtin.assert: + that: + - (repairs_response_21_before.json.ScheduledRepairs | map(attribute='Params') | map('first') | selectattr('tag', 'equalto', 'Test 21') | list | length) > 0 + fail_msg: "Repair with tag 'Test 21' was not created" + success_msg: "Repair with tag 'Test 21' exists, proceeding with deletion" + register: verify_result_21_before + when: delete_test_create_result.rc == 0 and repairs_response_21_before.json.ScheduledRepairs is defined + ignore_errors: true + + - name: "Test 21 - Step 3: Delete the scheduled repair" + ansible.builtin.shell: | + cd {{ cli_dir }} && pipenv run python axonops.py {{ common_args }} scheduledrepair \ + --delete \ + --tags 'Test 21' + register: delete_repair_result + when: delete_test_create_result.rc == 0 and (verify_result_21_before is not defined or not verify_result_21_before.failed) + ignore_errors: true + + - name: Display delete repair result + ansible.builtin.debug: + var: delete_repair_result + + - name: "Test 21 - Step 4: Fetch scheduled repairs from API after deletion" + ansible.builtin.uri: + url: "{{ repairs_api_url }}" + method: GET + headers: "{{ uri_headers }}" + user: "{{ axonops_username if not axonops_token and axonops_username else omit }}" + password: "{{ axonops_password if not axonops_token and axonops_password else omit }}" + force_basic_auth: "{{ true if not axonops_token and axonops_username else false }}" + return_content: true + status_code: [200, 201] + register: repairs_response_21_after + when: delete_repair_result is defined and delete_repair_result.rc == 0 + + - name: "Verify Test 21: Check scheduled repair was deleted" + ansible.builtin.assert: + that: + - (repairs_response_21_after.json.ScheduledRepairs | map(attribute='Params') | map('first') | selectattr('tag', 'equalto', 'Test 21') | list | length) == 0 + fail_msg: "Repair with tag 'Test 21' still exists after deletion" + success_msg: "Repair with tag 'Test 21' was successfully deleted" + register: verify_result_21 + when: delete_repair_result is defined and delete_repair_result.rc == 0 and repairs_response_21_after.json.ScheduledRepairs is defined + ignore_errors: true + # =========================================================================== # Summary and Final Assertion # =========================================================================== @@ -944,6 +1068,7 @@ ansible.builtin.set_fact: failed_tests: >- {{ + ([] if not clean_mode or (deleteall_result is defined and deleteall_result.rc == 0 and (verify_result_0 is not defined or not verify_result_0.failed)) else ['Test 0 - Delete all repairs']) + ([] if basic_repair_result.rc == 0 and (verify_result_1 is not defined or not verify_result_1.failed) else ['Test 1 - Basic repair']) + ([] if cron_repair_result.rc == 0 and (verify_result_2 is not defined or not verify_result_2.failed) else ['Test 2 - Cron expression']) + ([] if keyspace_repair_result.rc == 0 and (verify_result_3 is not defined or not verify_result_3.failed) else ['Test 3 - Specific keyspace']) + @@ -963,7 +1088,8 @@ ([] if tags_repair_result.rc == 0 and (verify_result_17 is not defined or not verify_result_17.failed) else ['Test 17 - Tags']) + ([] if paxos_only_result.rc == 0 and (verify_result_18 is not defined or not verify_result_18.failed) else ['Test 18 - Paxos only']) + ([] if skip_paxos_result.rc == 0 and (verify_result_19 is not defined or not verify_result_19.failed) else ['Test 19 - Skip paxos']) + - ([] if comprehensive_repair_result.rc == 0 and (verify_result_20 is not defined or not verify_result_20.failed) else ['Test 20 - Comprehensive']) + ([] if comprehensive_repair_result.rc == 0 and (verify_result_20 is not defined or not verify_result_20.failed) else ['Test 20 - Comprehensive']) + + ([] if (delete_test_create_result.rc == 0 and (delete_repair_result is defined and delete_repair_result.rc == 0) and (verify_result_21 is not defined or not verify_result_21.failed)) else ['Test 21 - Delete repair']) }} - name: Test summary @@ -972,7 +1098,8 @@ ============================================ Scheduled Repair Tests Summary ============================================ - Test 1 - Basic repair: {{ 'PASS' if basic_repair_result.rc == 0 and (verify_result_1 is not defined or not verify_result_1.failed) else 'FAIL' }} + {% if clean_mode %}Test 0 - Delete all repairs: {{ 'PASS' if (deleteall_result is defined and deleteall_result.rc == 0 and (verify_result_0 is not defined or not verify_result_0.failed)) else 'FAIL' }} + {% endif %}Test 1 - Basic repair: {{ 'PASS' if basic_repair_result.rc == 0 and (verify_result_1 is not defined or not verify_result_1.failed) else 'FAIL' }} Test 2 - Cron expression: {{ 'PASS' if cron_repair_result.rc == 0 and (verify_result_2 is not defined or not verify_result_2.failed) else 'FAIL' }} Test 3 - Specific keyspace: {{ 'PASS' if keyspace_repair_result.rc == 0 and (verify_result_3 is not defined or not verify_result_3.failed) else 'FAIL' }} Test 4 - Specific tables: {{ 'PASS' if tables_repair_result.rc == 0 and (verify_result_4 is not defined or not verify_result_4.failed) else 'FAIL' }} @@ -992,8 +1119,9 @@ Test 18 - Paxos only: {{ 'PASS' if paxos_only_result.rc == 0 and (verify_result_18 is not defined or not verify_result_18.failed) else 'FAIL' }} Test 19 - Skip paxos: {{ 'PASS' if skip_paxos_result.rc == 0 and (verify_result_19 is not defined or not verify_result_19.failed) else 'FAIL' }} Test 20 - Comprehensive: {{ 'PASS' if comprehensive_repair_result.rc == 0 and (verify_result_20 is not defined or not verify_result_20.failed) else 'FAIL' }} + Test 21 - Delete repair: {{ 'PASS' if (delete_test_create_result.rc == 0 and (delete_repair_result is defined and delete_repair_result.rc == 0) and (verify_result_21 is not defined or not verify_result_21.failed)) else 'FAIL' }} ============================================ - Total: {{ 20 - (failed_tests | length) }}/20 passed + Total: {{ (22 if clean_mode else 21) - (failed_tests | length) }}/{{ 22 if clean_mode else 21 }} passed ============================================ - name: Fail if any tests failed