library/calendar_sync.py aktualisiert
This commit is contained in:
@ -3,8 +3,24 @@
|
|||||||
from ansible.module_utils.basic import AnsibleModule
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
from caldav import DAVClient
|
from caldav import DAVClient
|
||||||
import vobject
|
import vobject
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
def connect_calendar(url, user=None, password=None, verify_ssl=True):
|
def fetch_ics_events(url, user=None, password=None):
|
||||||
|
"""Lädt Events aus einer ICS-URL"""
|
||||||
|
try:
|
||||||
|
auth = (user, password) if user and password else None
|
||||||
|
response = requests.get(url, auth=auth)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
ics_data = response.text
|
||||||
|
cal = vobject.readOne(ics_data)
|
||||||
|
return [comp for comp in cal.components() if comp.name == 'VEVENT']
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"ICS-Download fehlgeschlagen: {str(e)}")
|
||||||
|
|
||||||
|
def connect_caldav(url, user=None, password=None, verify_ssl=True):
|
||||||
|
"""Stellt Verbindung zum CalDAV-Kalender her"""
|
||||||
try:
|
try:
|
||||||
client = DAVClient(
|
client = DAVClient(
|
||||||
url,
|
url,
|
||||||
@ -12,53 +28,67 @@ def connect_calendar(url, user=None, password=None, verify_ssl=True):
|
|||||||
password=password,
|
password=password,
|
||||||
ssl_verify_cert=verify_ssl
|
ssl_verify_cert=verify_ssl
|
||||||
)
|
)
|
||||||
calendar = client.calendar(url=url)
|
# Kalender-URL aus Pfad extrahieren
|
||||||
# Test-Zugriff um sicherzustellen, dass der Kalender existiert
|
path = urlparse(url).path
|
||||||
calendar.get_properties(['displayname'])
|
principal = client.principal()
|
||||||
return calendar
|
for calendar in principal.calendars():
|
||||||
|
if path in calendar.url:
|
||||||
|
return calendar
|
||||||
|
raise Exception("Kalender nicht gefunden")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Kalenderverbindung fehlgeschlagen: {str(e)}")
|
raise Exception(f"CalDAV-Verbindungsfehler: {str(e)}")
|
||||||
|
|
||||||
def get_event_uid_map(calendar):
|
|
||||||
uid_map = {}
|
|
||||||
for event in calendar.events():
|
|
||||||
try:
|
|
||||||
cal = vobject.readOne(event.data)
|
|
||||||
uid = cal.vevent.uid.value
|
|
||||||
uid_map[uid] = event
|
|
||||||
except Exception as e:
|
|
||||||
continue # Fehlerhafte Events überspringen
|
|
||||||
return uid_map
|
|
||||||
|
|
||||||
def sync_calendars(source_cal, target_cal):
|
|
||||||
src_events = get_event_uid_map(source_cal)
|
|
||||||
tgt_events = get_event_uid_map(target_cal)
|
|
||||||
|
|
||||||
|
def sync_ics_to_caldav(module):
|
||||||
|
"""Haupt-Synchronisationslogik"""
|
||||||
|
# ICS-Events holen
|
||||||
|
ics_events = fetch_ics_events(
|
||||||
|
module.params['source_url'],
|
||||||
|
module.params['source_user'],
|
||||||
|
module.params['source_password']
|
||||||
|
)
|
||||||
|
|
||||||
|
# CalDAV-Ziel verbinden
|
||||||
|
target_cal = connect_caldav(
|
||||||
|
module.params['target_url'],
|
||||||
|
module.params['target_user'],
|
||||||
|
module.params['target_password'],
|
||||||
|
module.params['verify_ssl']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Existierende Events mappen
|
||||||
|
existing_events = {
|
||||||
|
event.icalendar_component['UID']: event
|
||||||
|
for event in target_cal.events()
|
||||||
|
}
|
||||||
|
|
||||||
changed = False
|
changed = False
|
||||||
added, updated, removed = [], [], []
|
results = {'added': [], 'updated': [], 'removed': []}
|
||||||
|
|
||||||
# Neue/aktualisierte Events
|
# Neue/aktualisierte Events syncen
|
||||||
for uid, src_event in src_events.items():
|
for vevent in ics_events:
|
||||||
tgt_event = tgt_events.get(uid)
|
uid = str(vevent['UID'].value)
|
||||||
src_data = src_event.data
|
ical_data = vevent.serialize()
|
||||||
|
|
||||||
if not tgt_event:
|
if uid not in existing_events:
|
||||||
target_cal.add_event(src_data)
|
target_cal.add_event(ical_data)
|
||||||
added.append(uid)
|
results['added'].append(uid)
|
||||||
changed = True
|
changed = True
|
||||||
elif src_data != tgt_event.data:
|
else:
|
||||||
tgt_event.data = src_data
|
# Nur aktualisieren wenn unterschiedlich
|
||||||
tgt_event.save()
|
if ical_data != existing_events[uid].data:
|
||||||
updated.append(uid)
|
existing_events[uid].data = ical_data
|
||||||
|
existing_events[uid].save()
|
||||||
|
results['updated'].append(uid)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
# Optionale Löschung nicht vorhandener Events
|
||||||
|
if module.params.get('purge'):
|
||||||
|
for uid in set(existing_events.keys()) - {str(e['UID'].value) for e in ics_events}:
|
||||||
|
existing_events[uid].delete()
|
||||||
|
results['removed'].append(uid)
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
# Entfernte Events löschen
|
return changed, results
|
||||||
for uid in set(tgt_events.keys()) - set(src_events.keys()):
|
|
||||||
tgt_events[uid].delete()
|
|
||||||
removed.append(uid)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
return changed, added, updated, removed
|
|
||||||
|
|
||||||
def run_module():
|
def run_module():
|
||||||
module_args = dict(
|
module_args = dict(
|
||||||
@ -68,52 +98,17 @@ def run_module():
|
|||||||
target_url=dict(type='str', required=True),
|
target_url=dict(type='str', required=True),
|
||||||
target_user=dict(type='str', required=False, default=None),
|
target_user=dict(type='str', required=False, default=None),
|
||||||
target_password=dict(type='str', required=False, no_log=True, default=None),
|
target_password=dict(type='str', required=False, no_log=True, default=None),
|
||||||
verify_ssl=dict(type='bool', required=False, default=True)
|
verify_ssl=dict(type='bool', default=True),
|
||||||
)
|
purge=dict(type='bool', default=False)
|
||||||
|
|
||||||
module = AnsibleModule(
|
|
||||||
argument_spec=module_args,
|
|
||||||
supports_check_mode=False
|
|
||||||
)
|
|
||||||
|
|
||||||
result = dict(
|
|
||||||
changed=False,
|
|
||||||
added=[],
|
|
||||||
updated=[],
|
|
||||||
removed=[]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(argument_spec=module_args, supports_check_mode=False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Kalender verbinden
|
changed, results = sync_ics_to_caldav(module)
|
||||||
source_cal = connect_calendar(
|
module.exit_json(changed=changed, **results)
|
||||||
url=module.params['source_url'],
|
|
||||||
user=module.params['source_user'],
|
|
||||||
password=module.params['source_password'],
|
|
||||||
verify_ssl=module.params['verify_ssl']
|
|
||||||
)
|
|
||||||
|
|
||||||
target_cal = connect_calendar(
|
|
||||||
url=module.params['target_url'],
|
|
||||||
user=module.params['target_user'],
|
|
||||||
password=module.params['target_password'],
|
|
||||||
verify_ssl=module.params['verify_ssl']
|
|
||||||
)
|
|
||||||
|
|
||||||
# Synchronisation durchführen
|
|
||||||
changed, added, updated, removed = sync_calendars(source_cal, target_cal)
|
|
||||||
|
|
||||||
result.update({
|
|
||||||
'changed': changed,
|
|
||||||
'added': added,
|
|
||||||
'updated': updated,
|
|
||||||
'removed': removed,
|
|
||||||
'msg': f"Synchronisiert: +{len(added)}/~{len(updated)}/-{len(removed)}"
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
module.fail_json(msg=f"Fehler bei der Synchronisation: {str(e)}", **result)
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
module.exit_json(**result)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
run_module()
|
run_module()
|
||||||
Reference in New Issue
Block a user