Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
```
6 changes: 5 additions & 1 deletion cli/axonopscli/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked this functionality, especially for testing purposes, but if we were creating one scheduled repair at a time, we would always delete all previously scheduled repairs. Instead, I put it behind a parameter flag.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks very good


scheduled_repair.set_options()

Expand Down
67 changes: 41 additions & 26 deletions cli/axonopscli/components/scheduled_repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was never done since GET doesn't support filtering by tags.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

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")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could have found adaptive repairs, but we're not looking at those.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, only the scheduled is needed here


def set_options(self):
"""Apply optional CLI parameters into the payload before sending it."""
Expand Down Expand Up @@ -101,8 +94,30 @@ def set_repair(self):
print("POST", self.full_add_repair_url, self.repair_data)

if self.args.delete:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We lost the ability to modify only a schedule with the same tag, instead of creating two separate schedules. Seeing if I can fix it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is sorted now

if self.args.v:
print("This scheduled repair is delete, not sending to AxonOps")
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 self.args.tags != repair['Params'][0]['tag']:
continue
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We filter out here to delete one tagged schedule repair at a time.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing scheduled repair mixed with tags and without. I had an error while deleting a tag:

$  pipenv run python axonops.py -v scheduledrepair --delete --tags asd
[...]
  File "/home/marionugnes/git/axonops-ansible-collection/cli/axonopscli/application.py", line 218, in run_scheduled_repair
    scheduled_repair.set_repair()
  File "/home/marionugnes/git/axonops-ansible-collection/cli/axonopscli/components/scheduled_repair.py", line 108, in set_repair
    if self.args.tags != repair['Params'][0]['tag']:
                         ~~~~~~~~~~~~~~~~~~~^^^^^^^
KeyError: 'tag'

I added a little if to check this case.

if self.args.v:
print(f"Deleting scheduled repair: {repair['ID']}")
self.remove_repair(repair['ID'])
if 'Params' in repair:
print(repair['Params'])
else:
print("No scheduled repairs found")
else:
if self.args.v:
print("Tags are always required for deletions")
raise
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double check what our parser logic does.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorted

return

self.axonops.do_request(
Expand Down
8 changes: 8 additions & 0 deletions tests/scheduled_repairs/run_scheduled_repair_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -36,13 +37,18 @@ while [[ $# -gt 0 ]]; do
INTERACTIVE=true
shift
;;
--clean)
CLEAN=true
shift
;;
--help)
echo "Usage: $0 [options]"
echo "Options:"
echo " --url URL AxonOps base URL (default: $DEFAULT_URL)"
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"
Expand All @@ -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
;;
*)
Expand Down Expand Up @@ -84,6 +91,7 @@ run_test() {
-e "org=$ORG" \
-e "cluster=$CLUSTER" \
-e "interactive=$INTERACTIVE" \
-e "clean=$CLEAN" \
-v
local exit_code=$?
set +x
Expand Down
134 changes: 131 additions & 3 deletions tests/scheduled_repairs/test_scheduled_repair.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >-
Expand All @@ -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)
# ===========================================================================
Expand Down Expand Up @@ -937,13 +982,93 @@
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
# ===========================================================================
- name: Calculate test results
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']) +
Expand All @@ -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
Expand All @@ -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' }}
Expand All @@ -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
Expand Down