Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
48 changes: 48 additions & 0 deletions DHIS2/usergroups_error_notificator/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# check_user_usergroup_size

This script is designed to monitor changes in users and user groups on a DHIS2 server. Its primary goal is to detect cache issues and notify when they have been resolved. Additionally, it handles legitimate changes in user groups and updates the data accordingly.

## How does the script work?

### Configuration loading

At the beginning, the script reads a configuration file that provides DHIS2 server credentials, including the server, username, and password.

### Creating the initial comparison file

If the `last_values.json` file does not exist, the script requests the current state of users and their groups from the server and saves it to this file. This ensures that there is always an initial state for future comparisons.

### Avoiding false positives

By filtering common users, the script ensures that any detected differences between the states are not due to new or removed users, but rather reflect actual changes in the user groups of existing users. This reduces the likelihood of false positives when detecting cache issues.

### Detecting recent changes in user groups

The script checks if there have been updates in the `userGroups` in the last 5 minutes. If recent changes are detected, the script ends as it doesn’t make sense to proceed if the user groups are being modified.

### State comparison

If no recent changes are found, the script compares the current state of users with the last saved state in `last_values.json`. If differences are detected, it could indicate a cache issue.

### Cache cleaning

If changes are detected, the script clears the DHIS2 server cache to see if this resolves the issue. After waiting for 30 seconds, it fetches the user data again.

### Post-cache cleaning verification

After cleaning the cache, the script compares the states again. If the issue is resolved (i.e., it was a cache issue), the new corrected state is saved, and the `control_file.json` is updated with the cache issue detection date.

### Handling legitimate changes

If the changes are not related to a cache issue, the script saves the new state without further modifications.

## What files does the script manage?

- **last_values.json**: Stores the last known state of users and their user groups for future comparisons.
- **control_file.json**: Stores the date when a cache issue was identified and resolved. This is used by Monit to trigger notifications.

## How to run the script

To execute the script, you need to pass the `--config` parameter followed by the path to the configuration file:

`python check_user_usergroup_size.py --config file.json`
173 changes: 173 additions & 0 deletions DHIS2/usergroups_error_notificator/check_user_usergroup_size.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import json
import sys
import time
import requests
from requests.auth import HTTPBasicAuth
from datetime import datetime, timedelta
import argparse
import os

class DHIS2Monitor:
def __init__(self, config):
self.server = config.get('server')
self.auth = HTTPBasicAuth(config.get('user'), config.get('password'))
self.interval = 30 # Interval of 30 seconds between comparisons

folder = config.get('folder')
self.last_values = os.path.join(folder, "last_values.json")
self.usergroups_file = os.path.join(folder, "usergroups.json")
self.control_file = os.path.join(folder, "control_file.json")

def request(self, url):
response = requests.get(url, auth=self.auth)
return response

def clean_cache(self):
return self.request(f'{self.server}api/38/maintenance?cacheClear=true')

def get_users_and_groups(self):
"""Fetches the list of users and their user group sizes."""
return self.request(f'{self.server}api/users?fields=id,userGroups::size&paging=false')

def user_groups_updated_recently(self):
"""Checks if any user groups were updated in the last 5 minutes and in that case exit."""
five_minutes_ago = (datetime.now() - timedelta(minutes=5)
).strftime("%Y-%m-%dT%H:%M:%S")
response = self.request(
f'{self.server}api/userGroups?filter=lastUpdated:gt:{five_minutes_ago}&paging=false')

if response.status_code == 200:
usergroups = response.json().get('userGroups', [])
return len(usergroups) > 0
else:
print(f"Error checking userGroups: {response.status_code} - {response.text}")
sys.exit(0)

def filter_common_users(self, current_users, previous_users):
"""Filters out added or deleted users to compare only common ones to avoid false positives."""
current_users_dict = {user['id']: user for user in current_users}
previous_users_dict = {user['id']: user for user in previous_users}

common_user_ids = set(current_users_dict.keys()).intersection(
set(previous_users_dict.keys()))
filtered_current_users = [current_users_dict[user_id]
for user_id in common_user_ids]
filtered_previous_users = [previous_users_dict[user_id]
for user_id in common_user_ids]

return filtered_current_users, filtered_previous_users

def save_state(self, filename, data):
"""Saves the given data to the specified file."""
with open(filename, 'w') as file:
json.dump(data, file, indent=4)

def load_state(self, filename):
"""Loads and returns data from the specified file."""
try:
with open(filename, 'r') as file:
return json.load(file)
except FileNotFoundError:
return {}

def states_are_different(self, state1, state2):
"""Compares two states after filtering common users excluding new or deleted users."""
state1, state2 = self.filter_common_users(state1, state2)
return state1 != state2

def get_date(self):
return datetime.now().strftime("%d_%m_%Y_%H_%M")

def handle_api_error(self):
print("API request failed")
sys.exit(0)

def save_control_file(self, date):
"""Saves the date in control file when a cache issue has been identified to be handled by monit."""
control_data = {"last_cache_issue_date": date}
with open(self.control_file, 'w') as file:
json.dump(control_data, file, indent=4)
print(f"Saved cache issue date to {self.control_file}")

def compare_states_and_save(self, data_before_clean_cache, data_after_cache_clean):
"""Handles the comparison of states and decides when to save."""
if self.states_are_different(data_before_clean_cache, data_after_cache_clean):
print("Clean cache fixed the problem. Saving the new state and updating control file.")
self.save_usergroups_to_analyze(self.get_date())
self.save_state(self.last_values, data_after_cache_clean)
self.save_control_file(self.get_date())
else:
print("No cache issue detected. Legitimate changes detected. Saving new state.")
self.save_state(self.last_values, data_after_cache_clean)

def save_usergroups_to_analyze(self, date):
"""Saves the user groups for analysis."""
response = self.request(f'{self.server}api/userGroups?fields=*&paging=false')
if response.status_code == 200:
usergroups_data = sorted(response.json().get(
'userGroups', []), key=lambda x: x['id'])
self.save_state(f"{date}_{self.usergroups_file}", usergroups_data)
return response

def create_last_changed_file_if_not_exists(self):
"""Creates last_changed.json if it doesn't exist with the current server data."""
if not self.load_state(self.last_values): # Check if file is empty or doesn't exist
print(f"{self.last_values} not found, creating it with the current state.")
response = self.get_users_and_groups()
if response.status_code == 200:
current_data = sorted(
response.json()["users"], key=lambda x: x['id'])
self.save_state(self.last_values, current_data)
print(f"Saved current state from the server to {self.last_values}.")
else:
print(f"Failed to fetch data from server to create {self.last_values}.")
sys.exit(0)


def main(config_path):
print("Starting at:" + datetime.now().strftime("%d_%m_%Y_%H_%M"))

with open(config_path, 'r') as conf_file:
config_data = json.load(conf_file)

monitor = DHIS2Monitor(config_data)

monitor.create_last_changed_file_if_not_exists()

# Check if any user groups were updated recently and exit if true
if monitor.user_groups_updated_recently():
print("User groups updated recently. Exiting.")
sys.exit(0)

last_changed_data = monitor.load_state(monitor.last_values)
response = monitor.get_users_and_groups()

if response.status_code != 200:
monitor.handle_api_error()

current_data = sorted(response.json()["users"], key=lambda x: x['id'])
print("Comparing current state.")

if monitor.states_are_different(last_changed_data, current_data):
print("Changes detected. Cleaning cache and waiting.")
monitor.clean_cache()
time.sleep(monitor.interval)

new_response = monitor.get_users_and_groups()
if new_response.status_code != 200:
monitor.handle_api_error()

current_data_after_cache_clean = sorted(
new_response.json()["users"], key=lambda x: x['id'])

monitor.compare_states_and_save(current_data, current_data_after_cache_clean)
else:
print("No changes detected.")


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="DHIS2 User-Usergroup notification script")
parser.add_argument('--config', required=True, help="Config file path")
args = parser.parse_args()
main(args.config)
6 changes: 6 additions & 0 deletions DHIS2/usergroups_error_notificator/example_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"server": "https://server/dhis2/",
"user": "username",
"password": "password",
"folder": "/path/to/save/control/files"
}