#!/usr/bin/env python3 from ansible.module_utils.basic import AnsibleModule from caldav import DAVClient import vobject import requests from urllib.parse import urlparse import logging # Logging für Debugging einrichten logging.basicConfig(level=logging.WARNING) def fetch_ics_events(url, user=None, password=None): try: response = requests.get(url, auth=(user, password) if user else None) response.raise_for_status() events = [] ics_data = response.text # Debug-Ausgabe für ungültige Daten if not ics_data.strip(): logging.warning("Leere ICS-Daten empfangen") return events try: for calendar in vobject.readComponents(ics_data): for component in calendar.components(): if component.name == 'VEVENT': if hasattr(component, 'uid') and component.uid.value: events.append(component) else: logging.warning("VEVENT ohne UID übersprungen") except vobject.base.ParseError as e: logging.error(f"Parse-Fehler in ICS-Daten: {str(e)}") return events except Exception as e: raise Exception(f"ICS-Fehler: {str(e)}") def connect_caldav(url, user=None, password=None, verify_ssl=False): try: client = DAVClient( url, username=user, password=password, ssl_verify_cert=verify_ssl ) principal = client.principal() target_path = urlparse(url).path # 1. Direkt versuchen mit bekannter Kalender-URL try: return principal.calendar(url) except Exception as direct_err: logging.warning(f"Direkter Zugriff auf Kalender fehlgeschlagen: {direct_err}") # 2. Fallback: Durch alle bekannten Kalender iterieren for calendar in principal.calendars(): if target_path in str(calendar.url): return calendar raise Exception(f"Kalender mit Pfad '{target_path}' nicht gefunden") except Exception as e: raise Exception(f"CalDAV-Fehler: {str(e)}") def safe_caldav_event_processing(event): try: if event is None or not hasattr(event, 'data') or not event.data: return None for calendar in vobject.readComponents(event.data): for component in calendar.components(): if component.name == 'VEVENT' and hasattr(component, 'uid'): return { 'uid': str(component.uid), 'event_object': event, 'data': event.data } return None except vobject.base.ParseError: logging.warning("Parse-Fehler in CalDAV-Event, überspringe") return None except Exception as e: logging.error(f"Fehler bei Event-Verarbeitung: {str(e)}") return None def sync_ics_to_caldav(module): ics_events = fetch_ics_events( module.params['source_url'], module.params['source_user'], module.params['source_password'] ) target_cal = connect_caldav( module.params['target_url'], module.params['target_user'], module.params['target_password'], module.params['verify_ssl'] ) existing_events = {} for event in target_cal.events(): processed = safe_caldav_event_processing(event) if processed: existing_events[processed['uid']] = processed changed = False results = {'added': [], 'updated': [], 'removed': []} for vevent in ics_events: uid = str(vevent.uid) try: ical_data = vevent.serialize() except Exception as e: logging.error(f"Serialisierungsfehler für UID {uid}: {str(e)}") continue if not ical_data.strip(): logging.warning(f"Leere iCal-Daten für UID {uid}, überspringe") continue if uid not in existing_events: try: target_cal.add_event(ical_data) results['added'].append(uid) changed = True except Exception as e: logging.error(f"Hinzufügen fehlgeschlagen für UID {uid}: {str(e)}") else: existing_data = existing_events[uid]['data'] if ical_data != existing_data: try: event_obj = existing_events[uid]['event_object'] event_obj.data = ical_data event_obj.save() results['updated'].append(uid) changed = True except Exception as e: logging.error(f"Aktualisieren fehlgeschlagen für UID {uid}: {str(e)}") if module.params['purge']: current_uids = {str(e.uid) for e in ics_events} for uid, event_info in existing_events.items(): if uid not in current_uids: try: event_info['event_object'].delete() results['removed'].append(uid) changed = True except Exception as e: logging.error(f"Löschen fehlgeschlagen für UID {uid}: {str(e)}") return changed, results def run_module(): module_args = dict( source_url=dict(type='str', required=True), source_user=dict(type='str', required=False, default=None), source_password=dict(type='str', required=False, no_log=True, default=None), target_url=dict(type='str', required=True), target_user=dict(type='str', required=False, default=None), target_password=dict(type='str', required=False, no_log=True, default=None), verify_ssl=dict(type='bool', default=False), purge=dict(type='bool', default=False) ) module = AnsibleModule(argument_spec=module_args, supports_check_mode=False) try: changed, results = sync_ics_to_caldav(module) module.exit_json(changed=changed, **results) except Exception as e: module.fail_json(msg=str(e)) if __name__ == '__main__': run_module()