Пример #1
0
def init_cgiscript_possibly_with_cert(print_header=True,
                                      content_type='text/html'):
    """Prepare for CGI script with optional client certificate. Only used from
    some of the cgi scripts still on the legacy-form like requestnewjob and
    put. I.e. scripts where certs are not required due to use of sessionid.
    """

    # Always rely on os.environ here since only called from cgi scripts
    environ = os.environ

    if print_header:
        cgiscript_header(content_type=content_type)

    configuration = get_configuration_object()
    logger = configuration.logger
    out = CGIOutput(logger)

    # get DN of user currently logged in

    client_id = extract_client_id(configuration, environ)
    if not client_id:
        logger.debug('(No client ID available in SSL session)')

    logger.info('script: %s cert: %s' % (requested_page(), client_id))
    return (logger, configuration, client_id, out)
Пример #2
0
def init_cgiscript_possibly_with_cert(print_header=True,
                                      content_type='text/html'):
    """Prepare for CGI script with optional client certificate. Only used from
    some of the cgi scripts still on the legacy-form like requestnewjob and
    put. I.e. scripts where certs are not required due to use of sessionid.
    """

    # Always rely on os.environ here since only called from cgi scripts
    environ = os.environ

    if print_header:
        cgiscript_header(content_type=content_type)

    configuration = get_configuration_object()
    logger = configuration.logger
    out = CGIOutput(logger)

    # get DN of user currently logged in

    client_id = extract_client_id(configuration, environ)
    if not client_id:
        logger.debug('(No client ID available in SSL session)')

    logger.info('script: %s cert: %s' % (requested_page(), client_id))
    return (logger, configuration, client_id, out)
Пример #3
0
def application(environ, start_response):
    """MiG app called automatically by wsgi"""

    # TODO: verify security of this environment exposure
    
    # pass environment on to sub handlers

    os.environ = environ

    # TODO: we should avoid print calls completely in backends
    # make sure print calls do not interfere with wsgi

    sys.stdout = sys.stderr
    configuration = get_configuration_object()

    # get and log ID of user currently logged in

    # We can't import helper before environ is ready because it indirectly
    # tries to use pre-mangled environ for conf loading
    
    from shared.httpsclient import extract_client_id
    client_id = extract_client_id(configuration, environ)

    fieldstorage = cgi.FieldStorage(fp=environ['wsgi.input'],
                                    environ=environ)
    user_arguments_dict = fieldstorage_to_dict(fieldstorage)

    # default to html

    output_format = 'html'
    if user_arguments_dict.has_key('output_format'):
        output_format = user_arguments_dict['output_format'][0]

    try:
        if not configuration.site_enable_wsgi:
            raise Exception("WSGI interface not enabled for this grid")
        
        # Environment contains python script _somewhere_ , try in turn
        # and fall back to dashboard if all fails
        script_path = requested_page(environ, 'dashboard.py')
        backend = os.path.basename(script_path).replace('.py' , '')
        module_path = 'shared.functionality.%s' % backend
        (output_objs, ret_val) = stub(module_path, configuration,
                                      client_id, user_arguments_dict, environ)
        status = '200 OK'
    except Exception, exc:
        status = '500 ERROR'
        (output_objs, ret_val) = ([{'object_type': 'title', 'text'
                                    : 'Unsupported Interface'},
                                   {'object_type': 'error_text', 'text'
                                    : str(exc)},
                                   # Enable next two lines only for debugging
                                   # {'object_type': 'text', 'text':
                                   # str(environ)}
                                   {'object_type': 'link', 'text':
                                    'Go to default interface',
                                    'destination': '/index.html'},
                                   ],
                                  returnvalues.SYSTEM_ERROR)
Пример #4
0
def init_cgi_script(environ, delayed_input=None):
    """Shared init"""
    configuration = get_configuration_object()
    logger = configuration.logger

    # get and log ID of user currently logged in

    client_id = extract_client_id(configuration, environ)
    logger.info('script: %s cert: %s' % (requested_page(), client_id))
    if not delayed_input:
        fieldstorage = cgi.FieldStorage()
        user_arguments_dict = fieldstorage_to_dict(fieldstorage)
    else:
        user_arguments_dict = {'__DELAYED_INPUT__': delayed_input}
    return (configuration, logger, client_id, user_arguments_dict)
Пример #5
0
def init_cgi_script(environ, delayed_input=None):
    """Shared init"""
    configuration = get_configuration_object()
    logger = configuration.logger

    # get and log ID of user currently logged in

    client_id = extract_client_id(configuration, environ)
    logger.info('script: %s cert: %s' % (requested_page(), client_id))
    if not delayed_input:
        fieldstorage = cgi.FieldStorage()
        user_arguments_dict = fieldstorage_to_dict(fieldstorage)
    else:
        user_arguments_dict = {'__DELAYED_INPUT__': delayed_input}
    return (configuration, logger, client_id, user_arguments_dict)
Пример #6
0
def initialize_main_variables(client_id, op_title=True, op_header=True, op_menu=True):
    """Script initialization is identical for most scripts in 
    shared/functionalty. This function should be called in most cases.
    """

    configuration = get_configuration_object()
    logger = configuration.logger
    output_objects = []
    start_entry = make_start_entry()
    output_objects.append(start_entry)
    op_name = os.path.basename(requested_page()).replace(".py", "")

    if op_title:
        title_object = make_title_entry("%s" % op_name, skipmenu=(not op_menu))
        output_objects.append(title_object)
    if op_header:
        header_object = make_header_entry("%s" % op_name)
        output_objects.append(header_object)
    if client_id:
        # add the user-defined menu and widgets (if possible)
        title = find_entry(output_objects, "title")
        if title:
            settings = load_settings(client_id, configuration)
            if settings:
                base_menu = settings.get("SITE_BASE_MENU", "default")
                if not base_menu in configuration.site_base_menu:
                    base_menu = "default"
                if base_menu == "simple" and configuration.site_simple_menu:
                    title["base_menu"] = configuration.site_simple_menu
                elif base_menu == "advanced" and configuration.site_advanced_menu:
                    title["base_menu"] = configuration.site_advanced_menu
                else:
                    title["base_menu"] = configuration.site_default_menu
                user_menu = settings.get("SITE_USER_MENU", None)
                if configuration.site_user_menu and user_menu:
                    title["user_menu"] = user_menu
                if settings.get("ENABLE_WIDGETS", True) and configuration.site_script_deps:
                    user_widgets = load_widgets(client_id, configuration)
                    if user_widgets:
                        title["user_widgets"] = user_widgets

    return (configuration, logger, output_objects, op_name)
Пример #7
0
def validate_input_and_cert(
    user_arguments_dict,
    defaults,
    output_objects,
    client_id,
    configuration,
    allow_rejects,
    require_user=True,
    filter_values=None,
    environ=None,
    ):
    """A wrapper used by most back end functionality - redirects to sign up
    if client_id is missing.
    """

    logger = configuration.logger
    if environ is None:
        environ = os.environ
    creds_error = ''
    if not client_id:
        creds_error = "Invalid or missing user credentials"
    elif require_user and not is_user(client_id, configuration.mig_server_home):
        creds_error = "No such user (%s)" % client_id

    if creds_error and not requested_page().endswith('logout.py'):
        output_objects.append({'object_type': 'error_text', 'text'
                              : creds_error
                              })

        # Redirect to sign-up cert page trying to guess relevant choices

        signup_url = os.path.join(configuration.migserver_https_sid_url,
                                  'cgi-sid', 'signup.py')
        signup_query = ''

        if not client_id:
            output_objects.append(
                {'object_type': 'text', 'text': '''Apparently you do not
already have access to %s, but you can sign up:''' % configuration.short_title
                 })
            output_objects.append({'object_type': 'link', 'text': signup_url,
                                   'destination': signup_url + signup_query})
            output_objects.append(
                {'object_type': 'text', 'text': '''If you already signed up and
received a user certificate you probably just need to import it in your
browser.'''})
        else:
            output_objects.append(
                {'object_type': 'text', 'text': '''Apparently you already have
suitable credentials and just need to sign up for a local %s account on:''' % \
                 configuration.short_title})

            if extract_client_cert(configuration, environ) is None:
                # Force logout/expire session cookie here to support signup
                identity = extract_client_openid(configuration, environ,
                                                 lookup_dn=False)
                if identity:
                    logger.info("expire openid user %s" % identity)
                    (success, _) = expire_oid_sessions(configuration, identity)
                else:
                    logger.info("no openid user logged in")
             
            output_objects.append({'object_type': 'link', 'text': signup_url,
                                   'destination': signup_url + signup_query})
        return (False, output_objects)

    (status, retval) = validate_input(user_arguments_dict, defaults,
            output_objects, allow_rejects, filter_values)

    return (status, retval)
Пример #8
0
def main(client_id, user_arguments_dict):
    """Main function used by front end"""

    (configuration, logger, output_objects, op_name) = \
        initialize_main_variables(client_id, op_header=False)
    output_objects.append({
        'object_type': 'header',
        'text': 'Grid Usage Statistics'
    })
    defaults = signature()[1]
    (validate_status, accepted) = validate_input(user_arguments_dict,
                                                 defaults,
                                                 output_objects,
                                                 allow_rejects=False)

    if not validate_status:
        return (accepted, returnvalues.CLIENT_ERROR)

    title_entry = find_entry(output_objects, 'title')
    title_entry['text'] = 'Usage Statistics'

    # read view options
    group_in_time = accepted['group_in_time'][-1]  # day, week, month
    time_start = accepted['time_start'][-1]
    time_end = accepted['time_end'][-1]
    display = accepted['display'][-1]  # machine, user, vgrid, summary

    if not configuration.site_enable_griddk:
        output_objects.append({
            'object_type':
            'text',
            'text':
            '''Grid.dk features are disabled on this site.
Please contact the site admins %s if you think they should be enabled.
''' % configuration.admin_email
        })
        return (output_objects, returnvalues.OK)

    # check arguments against configured lists of valid inputs:
    reject = False

    # make sure: grouping in ['user','machine', 'vgrid']
    if not display in displays:
        output_objects.append({
            'object_type':
            'error_text',
            'text':
            'invalid display grouping specified: %s' % display
        })
        display = displays[0]
        reject = True
    # make sure: group_in_time in ['all', 'month', 'day', 'week']
    if not group_in_time in time_groups:
        output_objects.append({
            'object_type':
            'error_text',
            'text':
            'invalid time grouping specified: %s' % group_in_time
        })
        group_in_time = time_groups[0]
        reject = True
    # make sure: start and end match "20[0-9]{2}-[01][0-9]"
    if not re.match('20\d\d-[01]\d', time_start):
        output_objects.append({
            'object_type':
            'error_text',
            'text':
            'invalid start time specified: %s' % time_start
        })
        time_start = '2009-09'
        reject = True

    if not re.match('20\d\d-[01]\d', time_end):
        output_objects.append({
            'object_type':
            'error_text',
            'text':
            'invalid end time specified: %s' % time_end
        })
        time_end = time.strftime('%Y-%m')
        reject = True

    # always include a form to re-display with different values:
    updateform = '           <form action="%s" >' %  \
                 os.path.basename(requested_page())
    updateform += '''
                <table class="runtimeenventry">
                  <thead>
                    <tr>
                    <th>Grouping by time</th>
                    <th>Start (YYYY-MM)</th>
                    <th>End (YYYY-MM)</th>
                    <th>Category</th>
                    <th><!-- dummy for update button --></th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr>
                      <td>
                        <select name="group_in_time">
'''
    #     updateform += \
    #      ''.join([ '\n<option value="' + t + '">' + t.title() + '</option>'
    #                for t in time_groups ])
    for t in time_groups:
        updateform += '<option '
        if group_in_time == t:
            updateform += 'selected '
        updateform += 'value="' + t + '">' + t.title() + '</option>\n'
    updateform += '''
                        </select>
                      </td>
                      <td><input type="text" name="time_start" value="%s"></td>
                      <td><input type="text" name="time_end" value="%s"></td>
''' % (time_start, time_end) + '''
                      <td><select name="display">
'''
    #     updateform += \
    #     ''.join([ '\n<option value="' + d + '">' + d.title() + '</option>'
    #                 for d in displays ])
    for d in displays:
        updateform += '<option '
        if display == d:
            updateform += 'selected '
        updateform += 'value="' + d + '">' + d.title() + '</option>\n'
    updateform += '''
                        </select>
                      </td>
                      <td><input type="submit" value="Update View"></td>
                    </tr>
                  </tbody>  
                </table>
            </form>
            <hr>
'''

    output_objects.append({'object_type': 'html_form', 'text': updateform})
    if reject:
        output_objects.append({
            'object_type': 'text',
            'text': 'Please check your view parameters.'
        })
        return (output_objects, returnvalues.CLIENT_ERROR)

    # else: all parameters OK, go:

    # determine the view and group level to use:

    # we use couchdb views with name convention <group-in-time>-<display>
    view = group_in_time + '-' + display

    # default group-level (max. 2 view components relevant)
    group_level = 2

    # could handle summary display specially, by setting group_level = 1
    # and using either <time>-machine or <time>-user view
    #    if display == 'summary':
    #        display = 'user'
    #        group_level = 1

    # construct start and end key.

    # machine and user cannot be filtered from the user, only
    # time can be specified, and as YYYY-MM only.
    # for view per week, we have to convert it to a week number
    # python starts by week 0, whereas javascript starts by week 1
    if group_in_time == 'week':
        t = time.strptime(time_start + '-07', "%Y-%m-%d")
        time_start = time.strftime("%Y,week%U", t)

    start_key = '["' + time_start.replace("-", " ") + '",null]'
    # 2nd component: user or machine
    # TODO allow only own user ID and only machines owned???
    # drawback: cannot restrict user/machine when requesting more than one time period
    # To restrict user/machine, views which have this as the first key part
    # have to be used. In which case only one user can be selected
    # to keep the time period selection valid.

    if group_in_time == 'week':
        # last week of the month = first week 1 month later
        # we better compute this instead of tweaking the input
        t = time.mktime(time.strptime(time_end + '-28', "%Y-%m-%d"))
        t += 7 * 24 * 3600
        end_key = '["' + \
            time.strftime("%Y,week%U", time.localtime(t)) + '",{}]'
    else:
        end_key = '["' + time_end.replace("-", " ") + ' 32' + '",{}]'
        # append last day, so inclusive end

    #  1. get json data from couchdb using the view
    #     group=true, group_level as calculated,
    #     start and end key as constructed

    # couchdb URL, default http://localhost:5984/
    # TODO use configuration.usagedb
    database = 'http://localhost:5984/usagerecords'

    [check, db_url, db_name] = database.rsplit('/', 2)
    if check and check != 'http:/':
        logger.debug('bad URL %s' % database)
        db_url = 'none'
        db_name = 'none'

    # views are organised in files per "timegrouping",
    # and contain views with names <category>-<timegrouping>
    query = '/'.join(['', db_name, '_design', group_in_time, '_view', view])
    query += '?'
    query += urllib.urlencode({
        'group': 'true',
        'group_level': group_level,
        'startkey': start_key,
        'endkey': end_key,
    })
    try:
        logger.debug("asking database at %s: %s" % (db_url, query))
        # Never use proxies
        res = urllib.urlopen('http://' + db_url + query, proxies={})
        jsonreply = res.read()
        res.close()
    except Exception, err:
        logger.error('Could not get data from database: %s' % err)
        output_objects.append({
            'object_type': 'error_text',
            'text': 'Error accessing the database.'
        })
        jsonreply = '{"rows":[]}'
Пример #9
0
def validate_input_and_cert(
    user_arguments_dict,
    defaults,
    output_objects,
    client_id,
    configuration,
    allow_rejects,
    require_user=True,
    filter_values=None,
    environ=None,
):
    """A wrapper used by most back end functionality - redirects to sign up
    if client_id is missing.
    """

    logger = configuration.logger
    if environ is None:
        environ = os.environ
    creds_error = ''
    if not client_id:
        creds_error = "Invalid or missing user credentials"
    elif require_user and not is_user(client_id,
                                      configuration.mig_server_home):
        creds_error = "No such user (%s)" % client_id

    if creds_error and not requested_page().endswith('logout.py'):
        output_objects.append({
            'object_type': 'error_text',
            'text': creds_error
        })

        if configuration.site_enable_gdp:
            main_url = configuration.migserver_http_url
            output_objects.append({
                'object_type':
                'text',
                'text':
                '''Apparently you do not
                        have access to this page, please return to:'''
            })

            output_objects.append({
                'object_type': 'link',
                'text': main_url,
                'destination': main_url
            })
        else:
            # Redirect to sign-up cert page trying to guess relevant choices

            signup_url = os.path.join(configuration.migserver_https_sid_url,
                                      'cgi-sid', 'signup.py')
            signup_query = ''

            if not client_id:
                output_objects.append({
                    'object_type':
                    'text',
                    'text':
                    '''Apparently you do not
    already have access to %s, but you can sign up:''' %
                    configuration.short_title
                })
                output_objects.append({'object_type': 'link', 'text':
                                       '%s sign up page' % \
                                       configuration.short_title,
                                       'destination': signup_url + signup_query})
                output_objects.append({
                    'object_type':
                    'text',
                    'text':
                    '''If you already signed up and
    received a user certificate you probably just need to import it in your
    browser.'''
                })
            else:
                output_objects.append({
                    'object_type':
                    'text',
                    'text':
                    '''Apparently you already have
    suitable credentials and just need to sign up for a local %s account on the'''
                    % configuration.short_title
                })

                base_url = extract_base_url(configuration, environ)
                if base_url == configuration.migserver_https_ext_cert_url and \
                       'extcert' in configuration.site_login_methods:
                    signup_query = '?show=extcert'
                elif base_url in (configuration.migserver_https_ext_oid_url,
                                  configuration.migserver_https_mig_oid_url):
                    # Force logout/expire session cookie here to support signup
                    (oid_db, identity) = extract_client_openid(configuration,
                                                               environ,
                                                               lookup_dn=False)
                    if oid_db and identity:
                        logger.info("openid expire user %s in %s" %
                                    (identity, oid_db))
                        (success,
                         _) = expire_oid_sessions(configuration, oid_db,
                                                  identity)
                        if oid_db == auth_openid_ext_db and \
                               'extoid' in configuration.site_signup_methods:
                            signup_query = '?show=extoid'
                        else:
                            logger.error("unknown migoid client_id %s on %s" \
                                         % (client_id, base_url))
                else:
                    logger.warning("unexpected client_id %s on %s" % \
                                   (client_id, base_url))

                output_objects.append({'object_type': 'link', 'text':
                                       '%s sign up page' % \
                                       configuration.short_title,
                                       'destination':
                                       signup_url + signup_query})
        return (False, output_objects)

    (status, retval) = validate_input(user_arguments_dict, defaults,
                                      output_objects, allow_rejects,
                                      filter_values)

    return (status, retval)
Пример #10
0
def initialize_main_variables(client_id,
                              op_title=True,
                              op_header=True,
                              op_menu=True):
    """Script initialization is identical for most scripts in 
    shared/functionalty. This function should be called in most cases.
    """

    configuration = get_configuration_object()
    logger = configuration.logger
    output_objects = []
    start_entry = make_start_entry()
    output_objects.append(start_entry)
    op_name = os.path.splitext(os.path.basename(requested_page()))[0]

    if op_title:
        skipwidgets = not configuration.site_enable_widgets or not client_id
        skipuserstyle = not configuration.site_enable_styling or not client_id
        title_object = make_title_entry('%s' % op_name,
                                        skipmenu=(not op_menu),
                                        skipwidgets=skipwidgets,
                                        skipuserstyle=skipuserstyle,
                                        skipuserprofile=(not client_id))
        # Make sure base_menu is always set for extract_menu
        # Typicall overriden based on client_id cases below
        title_object['base_menu'] = configuration.site_default_menu
        output_objects.append(title_object)
    if op_header:
        header_object = make_header_entry('%s' % op_name)
        output_objects.append(header_object)
    if client_id:
        # add the user-defined menu and widgets (if possible)
        title = find_entry(output_objects, 'title')
        if title:
            settings = load_settings(client_id, configuration)
            # NOTE: loaded settings may be False rather than dict here
            if not settings:
                settings = {}
            title['style'] = themed_styles(configuration,
                                           user_settings=settings)
            title['script'] = themed_scripts(configuration,
                                             user_settings=settings)
            if settings:
                title['user_settings'] = settings
                base_menu = settings.get('SITE_BASE_MENU', 'default')
                if not base_menu in configuration.site_base_menu:
                    base_menu = 'default'
                if base_menu == 'simple' and configuration.site_simple_menu:
                    title['base_menu'] = configuration.site_simple_menu
                elif base_menu == 'advanced' and \
                        configuration.site_advanced_menu:
                    title['base_menu'] = configuration.site_advanced_menu
                else:
                    title['base_menu'] = configuration.site_default_menu
                user_menu = settings.get('SITE_USER_MENU', None)
                if configuration.site_user_menu and user_menu:
                    title['user_menu'] = user_menu
                if settings.get('ENABLE_WIDGETS', True) and \
                        configuration.site_script_deps:
                    user_widgets = load_widgets(client_id, configuration)
                    if user_widgets:
                        title['user_widgets'] = user_widgets
            user_profile = load_profile(client_id, configuration)
            if user_profile:
                # These data are used for display in own profile view only
                profile_image_list = user_profile.get('PUBLIC_IMAGE', [])
                if profile_image_list:
                    # TODO: copy profile image to /public/avatars/X and use it
                    profile_image = os.path.join(
                        configuration.site_user_redirect,
                        profile_image_list[-1])
                else:
                    profile_image = ''
                user_profile['profile_image'] = profile_image
            else:
                user_profile = {}
            # Always set full name for use in personal user menu
            full_name = extract_field(client_id, 'full_name')
            user_profile['full_name'] = full_name
            title['user_profile'] = user_profile
            logger.debug('setting user profile: %s' % user_profile)
    else:
        # No user so we just enforce default site style and scripts
        title = find_entry(output_objects, 'title')
        if title:
            title['style'] = themed_styles(configuration)
            title['script'] = themed_scripts(configuration, logged_in=False)
    return (configuration, logger, output_objects, op_name)
Пример #11
0
def application(environ, start_response):
    """MiG app called automatically by wsgi"""

    # TODO: verify security of this environment exposure

    # pass environment on to sub handlers

    os.environ = environ

    # TODO: we should avoid print calls completely in backends
    # make sure print calls do not interfere with wsgi

    sys.stdout = sys.stderr
    configuration = get_configuration_object()
    _logger = configuration.logger

    # get and log ID of user currently logged in

    # We can't import helper before environ is ready because it indirectly
    # tries to use pre-mangled environ for conf loading

    from shared.httpsclient import extract_client_id
    client_id = extract_client_id(configuration, environ)

    fieldstorage = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ)
    user_arguments_dict = fieldstorage_to_dict(fieldstorage)

    # default to html

    output_format = 'html'
    if user_arguments_dict.has_key('output_format'):
        output_format = user_arguments_dict['output_format'][0]

    backend = "UNKNOWN"
    output_objs = []
    try:
        if not configuration.site_enable_wsgi:
            _logger.error("WSGI interface is disabled in configuration")
            raise Exception("WSGI interface not enabled for this site")

        # Environment contains python script _somewhere_ , try in turn
        # and fall back to dashboard if all fails
        script_path = requested_page(environ, configuration.site_landing_page)
        script_name = os.path.basename(script_path)
        backend = os.path.splitext(script_name)[0]
        module_path = 'shared.functionality.%s' % backend
        (allow, msg) = allow_script(configuration, script_name, client_id)
        if allow:
            (output_objs, ret_val) = stub(configuration, client_id,
                                          module_path, backend,
                                          user_arguments_dict, environ)
        else:
            (output_objs, ret_val) = reject_main(client_id,
                                                 user_arguments_dict)
        status = '200 OK'
    except Exception, exc:
        _logger.error("handling of WSGI request for %s from %s failed: %s" %
                      (backend, client_id, exc))
        status = '500 ERROR'
        crash_helper(configuration, backend, output_objs)
        output_objs.append({
            'object_type': 'link',
            'text': 'Go to default interface',
            'destination': configuration.site_landing_page
        })
        ret_val = returnvalues.SYSTEM_ERROR
Пример #12
0
def main(client_id, user_arguments_dict):
    """Main function used by front end"""

    (configuration, logger, output_objects, op_name) = \
        initialize_main_variables(client_id, op_header=False)
    output_objects.append({'object_type': 'header', 'text':
                           'Grid Usage Statistics'})
    defaults = signature()[1]
    (validate_status, accepted) = validate_input(user_arguments_dict,
            defaults, output_objects, allow_rejects=False)

    if not validate_status:
        return (accepted, returnvalues.CLIENT_ERROR)

    title_entry = find_entry(output_objects, 'title')
    title_entry['text'] = 'Usage Statistics'

    # read view options
    group_in_time = accepted['group_in_time'][-1] # day, week, month
    time_start = accepted['time_start'][-1]
    time_end = accepted['time_end'][-1]
    display = accepted['display'][-1] # machine, user, vgrid, summary

    if not configuration.site_enable_griddk:
        output_objects.append({'object_type': 'text', 'text':
                               '''Grid.dk features are disabled on this site.
Please contact the Grid admins %s if you think they should be enabled.
''' % configuration.admin_email})
        return (output_objects, returnvalues.OK)

    # check arguments against configured lists of valid inputs:
    reject = False

    # make sure: grouping in ['user','machine', 'vgrid']
    if not display in displays:
        output_objects.append({'object_type': 'error_text', 'text'
                   : 'invalid display grouping specified: %s' % display })
        display = displays[0]
        reject = True
    # make sure: group_in_time in ['all', 'month', 'day', 'week']
    if not group_in_time in time_groups:
        output_objects.append({'object_type': 'error_text', 'text'
                   : 'invalid time grouping specified: %s' % group_in_time })
        group_in_time = time_groups[0]
        reject = True
    # make sure: start and end match "20[0-9]{2}-[01][0-9]"
    if not re.match('20\d\d-[01]\d', time_start):
        output_objects.append({'object_type': 'error_text', 'text'
                   : 'invalid start time specified: %s' % time_start })
        time_start = '2009-09'
        reject = True

    if not re.match('20\d\d-[01]\d', time_end):
        output_objects.append({'object_type': 'error_text', 'text'
                    : 'invalid end time specified: %s' % time_end })
        time_end = time.strftime('%Y-%m')
        reject = True

    # always include a form to re-display with different values:
    updateform = '           <form action="%s" >' %  \
                 os.path.basename(requested_page())
    updateform +='''
                <table class="runtimeenventry">
                  <thead>
                    <tr>
                    <th>Grouping by time</th>
                    <th>Start (YYYY-MM)</th>
                    <th>End (YYYY-MM)</th>
                    <th>Category</th>
                    <th><!-- dummy for update button --></th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr>
                      <td>
                        <select name="group_in_time">
'''
#     updateform += \
#      ''.join([ '\n<option value="' + t + '">' + t.title() + '</option>'
#                for t in time_groups ])
    for t in time_groups:
        updateform +='<option '
        if group_in_time == t:
            updateform += 'selected '
        updateform += 'value="' + t + '">' + t.title() + '</option>\n'
    updateform +='''
                        </select>
                      </td>
                      <td><input type="text" name="time_start" value="%s"></td>
                      <td><input type="text" name="time_end" value="%s"></td>
''' %  (time_start, time_end) + '''
                      <td><select name="display">
'''
#     updateform += \
#     ''.join([ '\n<option value="' + d + '">' + d.title() + '</option>'
#                 for d in displays ])
    for d in displays:
        updateform +='<option '
        if display == d:
            updateform += 'selected '
        updateform += 'value="' + d + '">' + d.title() + '</option>\n'
    updateform +='''
                        </select>
                      </td>
                      <td><input type="submit" value="Update View"></td>
                    </tr>
                  </tbody>  
                </table>
            </form>
            <hr>
'''

    output_objects.append({'object_type': 'html_form', 'text': updateform})
    if reject:
        output_objects.append({'object_type': 'text', 'text'
                   : 'Please check your view parameters.'})
        return(output_objects, returnvalues.CLIENT_ERROR)

    # else: all parameters OK, go:

    # determine the view and group level to use:

    # we use couchdb views with name convention <group-in-time>-<display>
    view = group_in_time + '-' + display

    # default group-level (max. 2 view components relevant)
    group_level = 2

    # could handle summary display specially, by setting group_level = 1
    # and using either <time>-machine or <time>-user view
#    if display == 'summary':
#        display = 'user'
#        group_level = 1

    # construct start and end key.

    # machine and user cannot be filtered from the user, only 
    # time can be specified, and as YYYY-MM only.
    # for view per week, we have to convert it to a week number
    # python starts by week 0, whereas javascript starts by week 1
    if group_in_time == 'week':
        t = time.strptime(time_start + '-07',"%Y-%m-%d")
        time_start = time.strftime("%Y,week%U",t)

    start_key = '["'+ time_start.replace("-"," ") + '",null]'
    # 2nd component: user or machine
    # TODO allow only own user ID and only machines owned???
    # drawback: cannot restrict user/machine when requesting more than one time period 
    # To restrict user/machine, views which have this as the first key part 
    # have to be used. In which case only one user can be selected 
    # to keep the time period selection valid.

    if group_in_time == 'week':
        # last week of the month = first week 1 month later
        # we better compute this instead of tweaking the input
        t = time.mktime(time.strptime(time_end + '-28',"%Y-%m-%d"))
        t += 7*24*3600
        end_key = '["'+ time.strftime("%Y,week%U",time.localtime(t)) + '",{}]'
    else:
        end_key = '["'+ time_end.replace("-"," ") + ' 32' + '",{}]'
        # append last day, so inclusive end

    #  1. get json data from couchdb using the view
    #     group=true, group_level as calculated,
    #     start and end key as constructed

    # couchdb URL, default http://localhost:5984/
    # TODO use configuration.usagedb
    database ='http://localhost:5984/usagerecords' 

    [check,db_url,db_name] = database.rsplit('/',2)
    if check and check != 'http:/':
        logger.debug('bad URL %s' % database)
        db_url = 'none'
        db_name = 'none'
    
    # views are organised in files per "timegrouping",
    # and contain views with names <category>-<timegrouping>
    query = '/'.join(['',db_name,'_design' ,group_in_time,'_view',view])
    query += '?'
    query += urllib.urlencode({'group'      : 'true',
                               'group_level': group_level,
                               'startkey'   : start_key,
                               'endkey'     : end_key,
                               })
    try:
        logger.debug("asking database at %s: %s" % (db_url,query))
        res = urllib.urlopen('http://' + db_url + query)
        jsonreply = res.read()
        res.close()
    except Exception, err:
        logger.error('Could not get data from database: %s' % err)
        output_objects.append({'object_type': 'error_text', 'text'
                   : 'Error accessing the database.' })
        jsonreply = '{"rows":[]}'