def http_request(url, post_data=None):
    '''Make an HTTP request to a given URL with optional parameters.
    '''
    logger.debug('Requesting URL: %s', url)
    buf = bio()
    curl = pycurl.Curl()
    curl.setopt(curl.URL, url.encode('ascii', 'ignore'))

    # More verbose curl calls in debug mode
    if logger.getEffectiveLevel() == logging.DEBUG:
        curl.setopt(pycurl.VERBOSE, True)

    # Disable HTTPS verification methods if insecure is set
    if config('server', 'insecure'):
        curl.setopt(curl.SSL_VERIFYPEER, 0)
        curl.setopt(curl.SSL_VERIFYHOST, 0)

    if config('server', 'certificate'):
        # Make sure verification methods are turned on
        curl.setopt(curl.SSL_VERIFYPEER, 1)
        curl.setopt(curl.SSL_VERIFYHOST, 2)
        # Import your certificates
        curl.setopt(pycurl.CAINFO, config('server', 'certificate'))

    if config('ingest', 'upload_rate') > 0:
        curl.setopt(
            curl.MAX_SEND_SPEED_LARGE,
            config('ingest', 'upload_rate'))

    if post_data:
        curl.setopt(curl.HTTPPOST, post_data)
    curl.setopt(curl.WRITEFUNCTION, buf.write)
    logger.debug('Using authentication method %s',
                 config('server')['auth_method'])
    if config('server')['auth_method'] == 'digest':
        curl.setopt(curl.HTTPHEADER, ['X-Requested-Auth: Digest'])
        curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_DIGEST)
    curl.setopt(pycurl.USERPWD, ':'.join([config('server', 'username'),
                                          config('server', 'password')]))
    curl.setopt(curl.FAILONERROR, True)
    curl.setopt(curl.FOLLOWLOCATION, True)
    curl.perform()
    curl.close()
    result = buf.getvalue()
    buf.close()
    return result def service(service_name, force_update=False):
    '''Get the location of a given service from Opencast and add it to the
    current configuration.

    :param service_name: Name of the service type to request locations for
    :param force_update: Force an update, possibly wait for a service to
                         become available if none is available right now.
    :return: List of service locations
    '''
    service_id = f'org.opencastproject.{service_name}'
    service_url = config('services', service_id)
    logger.debug('Cached service URLs for %s: %s', service_name, service_url)
    if service_url and not force_update:
        return service_url

    # Get service from Opencast server
    config('services')[service_id] = []
    while not config('services', service_id) and not terminate():
        try:
            config('services')[service_id] = get_service(service_id)
            logger.debug('Updates service URL for %s: %s',
                         service_name,
                         config('services', service_id))
        except pycurl.error:
            logger.exception('Could not get %s endpoint. Retry in 5s',
                             service_name)
            time.sleep(5.0)
    return config('services', service_id) def timestamp():
    '''Get current unix timestamp
    '''
    return int(datetime.now(tzutc()).timestamp()) def terminate(shutdown=None):
    '''Mark process as to be terminated.
    '''
    global _terminate
    if shutdown is not None:
        _terminate = shutdown
    return '_terminate' in globals() and _terminate def set_service_status_immediate(service, status):
    '''Update the status of a particular service in the database and send an
    immediate signal to Opencast.
    '''
    set_service_status(service, status)
    update_agent_state() def config(*args):
    '''Get a specific configuration value or the whole configuration, loading
    the configuration file if it was not before.

    :param key: optional configuration key to return
    :type key: string
    :return: Part of the configuration object containing the configuration
             or configuration value.
             If a part of the configuration object (e.g. configobj.Section) is
             returned, it can be treated like a dictionary.
             Returning None, if the configuration value is not found.
    '''
    cfg = __config or update_configuration()
    for key in args:
        if cfg is None:
            return
        cfg = cfg.get(key)
    return cfg def get_session():
    '''Get a session for database communication. If necessary a new connection
    to the database will be established.

    :return:  Database session
    '''
    if 'engine' not in globals():
        init()
    Session = sessionmaker(bind=engine)
    return Session() class UpcomingEvent(Base, BaseEvent):
    '''List of upcoming events'''

    __tablename__ = 'upcoming_event' class Service(Constants):
    '''Service type definitions
    '''
    AGENTSTATE = 1
    CAPTURE = 2
    INGEST = 3
    SCHEDULE = 4 class ServiceStatus(Constants):
    '''Service status type definitions
    '''
    STOPPED = 1
    IDLE = 2
    BUSY = 3 class UpstreamState(Base):
    '''State of the upstream Opencast server.'''
    __tablename__ = 'upstream_state'
    url = Column('url', Text(), primary_key=True)
    last_synced = Column('last_synced', DateTime())

    @staticmethod
    def update_sync_time(url):
        s = get_session()
        s.merge(UpstreamState(url=url, last_synced=datetime.utcnow()))
        s.commit()
        s.close() def with_session(f):
    """Wrapper for f to make a SQLAlchemy session present within the function

    :param f: Function to call
    :type f: Function
    :raises e: Possible exception of f
    :return: Result of f
    """
    @wraps(f)
    def decorated(*args, **kwargs):
        session = get_session()
        try:
            result = f(session, *args, **kwargs)
        except Exception as e:
            session.rollback()
            raise e
        finally:
            session.close()
        return result
    return decorated
from pyca.utils import http_request, service, timestamp, terminate, \
                       set_service_status_immediate
from pyca.config import config
from pyca.db import get_session, UpcomingEvent, Service, ServiceStatus, \
    UpstreamState, with_session
from base64 import b64decode
from datetime import datetime
from urllib.parse import urlencode
import dateutil.parser
import logging
import pycurl
import sdnotify
import time
# -*- coding: utf-8 -*-
'''
    python-capture-agent
    ~~~~~~~~~~~~~~~~~~~~

    :copyright: 2014-2017, Lars Kiesow <lkiesow@uos.de>
    :license: LGPL – see license.lgpl for more details.
'''


logger = logging.getLogger(__name__)
notify = sdnotify.SystemdNotifier()


def parse_ical(vcal):
    '''Parse Opencast schedule iCalendar file and return events as dict
    '''
    vcal = vcal.replace('\r\n ', '').replace('\r\n\r\n', '\r\n')
    vevents = vcal.split('\r\nBEGIN:VEVENT\r\n')
    del(vevents[0])
    events = []
    for vevent in vevents:
        event = {}
        for line in vevent.split('\r\n'):
            line = line.split(':', 1)
            key = line[0].lower()
            if len(line) <= 1 or key == 'end':
                continue
            if key.startswith('dt'):
                event[key] = int(dateutil.parser.parse(line[1]).timestamp())
                continue
            if not key.startswith('attach'):
                event[key] = line[1]
                continue
            # finally handle attachments
            event['attach'] = event.get('attach', [])
            attachment = {}
            for x in [x.split('=') for x in line[0].split(';')]:
                if x[0].lower() in ['fmttype', 'x-apple-filename']:
                    attachment[x[0].lower()] = x[1]
            attachment['data'] = b64decode(line[1]).decode('utf-8')
            event['attach'].append(attachment)
        events.append(event)
    return events


@with_session
def get_schedule(db):
    '''Try to load schedule from the Matterhorn core. Returns a valid schedule
    or None on failure.
    '''
    params = {'agentid': config('agent', 'name').encode('utf8')}
    lookahead = config('agent', 'cal_lookahead') * 24 * 60 * 60
    if lookahead:
        params['cutoff'] = str((timestamp() + lookahead) * 1000)
    uri = '%s/calendars?%s' % (service('scheduler')[0],
                               urlencode(params))
    try:
        vcal = http_request(uri)
        UpstreamState.update_sync_time(config('server', 'url'))
    except pycurl.error as e:
        logger.error('Could not get schedule: %s', e)
        return

    try:
        cal = parse_ical(vcal.decode('utf-8'))
    except Exception:
        logger.exception('Could not parse ical')
        return
    db.query(UpcomingEvent).delete()
    for event in cal:
        # Ignore events that have already ended
        if event['dtend'] <= timestamp():
            continue
        e = UpcomingEvent()
        e.start = event['dtstart']
        e.end = event['dtend']
        e.uid = event.get('uid')
        e.title = event.get('summary')
        e.set_data(event)
        db.add(e)
    db.commit()


def control_loop():
    '''Main loop, retrieving the schedule.
    '''

    set_service_status_immediate(Service.SCHEDULE, ServiceStatus.BUSY)