Пример #1
0
def add_ssh_public_key(username):
    """
    Adds a new SSH public key for the given user account.

    Accepts mimetype:`text/plain` request bodies containing the SSH public key 
    in the conventional OpenSSH format: <keytype> <key> <ident>.

    :param username: The user's username.
    """
    user = _get_user(username)
    if not user.can_edit(identity.current.user):
        raise Forbidden403('Cannot edit user %s' % user)
    if request.mimetype != 'text/plain':
        raise UnsupportedMediaType415('Request content type must be text/plain')
    with convert_internal_errors():
        keytext = request.data.strip()
        if '\n' in keytext:
            raise ValueError('SSH public keys may not contain newlines')
        elements = keytext.split(None, 2)
        if len(elements) != 3:
            raise ValueError('Invalid SSH public key')
        key = SSHPubKey(*elements)
        user.sshpubkeys.append(key)
        session.flush() # to populate id
    return jsonify(key.__json__())
Пример #2
0
def create_pool():
    """
    Creates a new system pool in Beaker. The request must be 
    :mimetype:`application/x-www-form-urlencoded` or 
    :mimetype:`application/json`.

    :jsonparam string name: Name for the system pool.
    :jsonparam string description: Description of the system pool.
    :jsonparam object owner: JSON object containing a ``user_name`` key or
      ``group_name`` key identifying the owner for the system pool.

    :status 201: The system pool was successfully created.
    """
    owner = None
    description = None
    u = identity.current.user
    if request.json:
        if 'name' not in request.json:
            raise BadRequest400('Missing pool name key')
        new_name = request.json['name']
        if 'owner' in request.json:
            owner = request.json['owner']
        if 'description' in request.json:
            description = request.json['description']
    elif request.form:
        if 'name' not in request.form:
            raise BadRequest400('Missing pool name parameter')
        new_name = request.form['name']
        if 'owner' in request.form:
            owner = request.form['owner']
        if 'description' in request.form:
            description = request.form['description']
    else:
        raise UnsupportedMediaType415
    with convert_internal_errors():
        if SystemPool.query.filter(SystemPool.name == new_name).count() != 0:
            raise Conflict409('System pool with name %r already exists' %
                              new_name)
        pool = SystemPool(name=new_name, description=description)
        session.add(pool)
        if owner:
            owner, owner_type = _get_owner(owner)
            if owner_type == 'user':
                pool.owning_user = owner
            else:
                pool.owning_group = owner
        else:
            pool.owning_user = u
        # new systems pool are visible to everybody by default
        pool.access_policy = SystemAccessPolicy()
        pool.access_policy.add_rule(SystemPermission.view, everybody=True)
        pool.record_activity(user=u,
                             service=u'HTTP',
                             action=u'Created',
                             field=u'Pool',
                             new=unicode(pool))
    response = jsonify(pool.__json__())
    response.status_code = 201
    response.headers.add('Location', absolute_url(pool.href))
    return response
Пример #3
0
def add_system():
    # We accept JSON or form-encoded for convenience
    if request.json:
        if 'fqdn' not in request.json:
            raise BadRequest400('Missing fqdn key')
        new_fqdn = request.json['fqdn']
    elif request.form:
        if 'fqdn' not in request.form:
            raise BadRequest400('Missing fqdn parameter')
        new_fqdn = request.form['fqdn']
    else:
        raise UnsupportedMediaType415
    with convert_internal_errors():
        if System.query.filter(System.fqdn == new_fqdn).count() != 0:
            raise Conflict409('System with fqdn %r already exists' % new_fqdn)
        system = System(fqdn=new_fqdn, owner=identity.current.user)
        session.add(system)
        # new systems are visible to everybody by default
        system.custom_access_policy = SystemAccessPolicy()
        system.custom_access_policy.add_rule(SystemPermission.view,
                                             everybody=True)
    # XXX this should be 201 with Location: /systems/FQDN/ but 302 is more
    # convenient because it lets us use a traditional browser form without AJAX
    # handling, and for now we're redirecting to /view/FQDN until that is moved
    # to /systems/FQDN/
    return flask_redirect(url(u'/view/%s#essentials' % system.fqdn))
Пример #4
0
def _create_labcontroller_helper(data):
    with convert_internal_errors():
        if LabController.query.filter_by(fqdn=data['fqdn']).count():
            raise Conflict409('Lab Controller %s already exists' % data['fqdn'])

        user = find_user_or_create(data['user_name'])
        user = update_user(
            user=user,
            display_name=data['fqdn'],
            email_address=data.get('email_address', user.email_address),
            password=data.get('password', user.password)
        )
        labcontroller = LabController(fqdn=data['fqdn'], disabled=False)
        labcontroller.record_activity(
            user=identity.current.user, service=u'HTTP',
            action=u'Changed', field=u'FQDN', old=u'', new=data['fqdn'])

        labcontroller.user = user
        labcontroller.record_activity(
            user=identity.current.user, service=u'HTTP',
            action=u'Changed', field=u'User', old=u'', new=user.user_name)

        # For backwards compatibility
        labcontroller.record_activity(
            user=identity.current.user, service=u'HTTP',
            action=u'Changed', field=u'Disabled', old=u'', new=unicode(labcontroller.disabled))

        session.add(labcontroller)
        # flush it so we return an id, otherwise we'll end up back in here from
        # the edit form
        session.flush()

    response = jsonify(labcontroller.__json__())
    response.status_code = 201
    return response
Пример #5
0
def doit():
    distro_trees = []
    for id in request.form.getlist('distro_tree_id'):
        try:
            distro_trees.append(DistroTree.by_id(id))
        except NoResultFound:
            raise BadRequest400('Distro tree %r does not exist' % id)
    job_details = {}
    job_details['pick'] = request.form.get('pick') or 'auto'
    if job_details['pick'] == 'fqdn':
        try:
            job_details['system'] = System.by_fqdn(request.form.get('system'),
                                                   identity.current.user)
        except NoResultFound:
            raise BadRequest400('System %s not found' %
                                request.form.get('system'))
    elif job_details['pick'] == 'lab':
        try:
            job_details['lab'] = LabController.by_name(request.form.get('lab'))
        except NoResultFound:
            raise BadRequest400('Lab controller %s not found' %
                                request.form.get('lab'))
    days = int(request.form.get('reserve_days') or DEFAULT_RESERVE_DAYS)
    days = min(days, MAX_DAYS_PROVISION)
    job_details['reservetime'] = days * 24 * 60 * 60
    job_details['whiteboard'] = request.form.get('whiteboard')
    job_details['ks_meta'] = request.form.get('ks_meta')
    job_details['koptions'] = request.form.get('koptions')
    job_details['koptions_post'] = request.form.get('koptions_post')
    with convert_internal_errors():
        job = Job.provision_system_job(distro_trees, **job_details)
    return 'Created %s' % job.t_id, 201, [('Location',
                                           url('/jobs/%s' % job.id))]
Пример #6
0
def provision_system(fqdn):
    system = _get_system_by_FQDN(fqdn)
    if not system.can_configure_netboot(identity.current.user):
        raise Forbidden403('Cannot provision system')
    data = read_json_request(request)
    with convert_internal_errors():
        if not data['distro_tree'] or 'id' not in data['distro_tree']:
            raise ValueError('No distro tree specified')
        distro_tree = DistroTree.by_id(data['distro_tree']['id'])
        user = identity.current.user
        if user.rootpw_expired:
            raise ValueError('Your root password has expired, you must '
                    'change or clear it in order to provision.')
            redirect(u"/view/%s" % system.fqdn)
        install_options = system.manual_provision_install_options(distro_tree)\
                .combined_with(InstallOptions.from_strings(data.get('ks_meta'),
                    data.get('koptions'), data.get('koptions_post')))
        if 'ks' not in install_options.kernel_options:
            kickstart = generate_kickstart(install_options,
                    distro_tree=distro_tree, system=system, user=user)
            install_options.kernel_options['ks'] = kickstart.link
        system.configure_netboot(distro_tree,
                install_options.kernel_options_str, service=u'HTTP')
        system.record_activity(user=identity.current.user, service=u'HTTP',
                action=u'Provision', field=u'Distro Tree',
                new=unicode(distro_tree))
        if data.get('reboot'):
            system.action_power(action=u'reboot', service=u'HTTP')
    # in future "installations" will be a real thing in our model,
    # but for now we have nothing to return
    return 'Provisioned', 201
Пример #7
0
def doit():
    distro_trees = []
    for id in request.form.getlist('distro_tree_id'):
        try:
            distro_trees.append(DistroTree.by_id(id))
        except NoResultFound:
            raise BadRequest400('Distro tree %r does not exist' % id)
    job_details = {}
    job_details['pick'] = request.form.get('pick') or 'auto'
    if job_details['pick'] == 'fqdn':
        try:
            job_details['system'] = System.by_fqdn(request.form.get('system'),
                    identity.current.user)
        except NoResultFound:
            raise BadRequest400('System %s not found' % request.form.get('system'))
    elif job_details['pick'] == 'lab':
        try:
            job_details['lab'] = LabController.by_name(request.form.get('lab'))
        except NoResultFound:
            raise BadRequest400('Lab controller %s not found' % request.form.get('lab'))
    days = int(request.form.get('reserve_days') or DEFAULT_RESERVE_DAYS)
    days = min(days, MAX_DAYS_PROVISION)
    job_details['reservetime'] = days * 24 * 60 * 60
    job_details['whiteboard'] = request.form.get('whiteboard')
    job_details['ks_meta'] = request.form.get('ks_meta')
    job_details['koptions'] = request.form.get('koptions')
    job_details['koptions_post'] = request.form.get('koptions_post')
    with convert_internal_errors():
        job = Job.provision_system_job(distro_trees, **job_details)
    return 'Created %s' % job.t_id, 201, [('Location', absolute_url('/jobs/%s' % job.id))]
Пример #8
0
def add_ssh_public_key(username):
    """
    Adds a new SSH public key for the given user account.

    Accepts mimetype:`text/plain` request bodies containing the SSH public key 
    in the conventional OpenSSH format: <keytype> <key> <ident>.

    :param username: The user's username.
    """
    user = _get_user(username)
    if not user.can_edit(identity.current.user):
        raise Forbidden403('Cannot edit user %s' % user)
    if request.mimetype != 'text/plain':
        raise UnsupportedMediaType415(
            'Request content type must be text/plain')
    with convert_internal_errors():
        keytext = request.data.strip()
        if '\n' in keytext:
            raise ValueError('SSH public keys may not contain newlines')
        elements = keytext.split(None, 2)
        if len(elements) != 3:
            raise ValueError('Invalid SSH public key')
        key = SSHPubKey(*elements)
        user.sshpubkeys.append(key)
        session.flush()  # to populate id
    return jsonify(key.__json__())
Пример #9
0
def update_recipeset_status(id):
    """
    Updates the status of a recipe set. The request must be :mimetype:`application/json`.

    Currently the only allowed value for status is 'Cancelled', which has the 
    effect of cancelling all recipes in the recipe set that have not finished yet.

    :param id: ID of the recipe set.
    :jsonparam string status: The new status. Must be 'Cancelled'.
    :jsonparam string msg: A message describing the reason for updating the status.
    """
    recipeset = _get_rs_by_id(id)
    if not recipeset.can_cancel(identity.current.user):
        raise Forbidden403('Cannot update recipe set status')
    data = read_json_request(request)
    if 'status' not in data:
        raise BadRequest400('Missing status')
    status = TaskStatus.from_string(data['status'])
    msg = data.get('msg', None) or None
    if status != TaskStatus.cancelled:
        raise BadRequest400('Status must be "Cancelled"')
    with convert_internal_errors():
        recipeset.record_activity(user=identity.current.user,
                                  service=u'HTTP',
                                  field=u'Status',
                                  action=u'Cancelled')
        recipeset.cancel(msg=msg)
    return '', 204
Пример #10
0
def exclude_user(group_name):
    """
    Exclude a user from an inverted group. Then the user will not have the group
    membership.

    :param group_name: Group's name.
    :jsonparam string user_name: User's username.

    """
    u = identity.current.user
    data = read_json_request(request)
    group = _get_group_by_name(group_name, lockmode='update')
    if not group.can_modify_membership(identity.current.user):
        raise Forbidden403('Cannot edit membership of group %s' % group_name)
    if group.membership_type == GroupMembershipType.normal:
        raise NotFound404('Normal group %s do not have excluded users' %
                          group_name)
    if 'user_name' not in data:
        raise BadRequest400('User not specified')
    user = _get_user_by_username(data['user_name'])
    if user in group.users:
        if not group.can_exclude_member(u, user.id):
            raise Forbidden403('Cannot exclude user %s from group %s' %
                               (user, group_name))
        with convert_internal_errors():
            group.exclude_user(user, agent=identity.current.user)
    else:
        raise Conflict409('User %s is already excluded from group %s' %
                          (user.user_name, group_name))
    return '', 204
Пример #11
0
def update_task(task_id):
    """
    Updates a task - only handles disabling at this time.

    :param task_id: The task id to update/disable
    :jsonparam bool disabled: Whether the task should be disabled.
    :status 200: Task was successfully updated/disabled
    :status 404: Task was not found (to be disabled)
    """
    try:
        task = Task.by_id(task_id)
    except DatabaseLookupError as e:
        # This should be NotFound404 but due to still using cherrypy
        # 404's are handled there which then will then do a GET /tasks/id
        # which will resolve correctly, which isn't desired
        raise NotFound404('Task %s does not exist' % task_id)

    data = read_json_request(request)

    if data:
        with convert_internal_errors():
            if data.get('disabled', False) and task.valid:
                task.disable()

    response = jsonify(task.to_dict())

    return response
Пример #12
0
def update_recipe(id):
    """
    Updates the attributes of a recipe. The request must be 
    :mimetype:`application/json`.

    :param id: Recipe's id.
    :jsonparam string whiteboard: Whiteboard of the recipe.
    :status 200: Recipe was updated.
    :status 400: Invalid data was given.
    """

    recipe = _get_recipe_by_id(id)
    if not recipe.can_edit(identity.current.user):
        raise Forbidden403('Cannot edit recipe %s' % recipe.id)
    data = read_json_request(request)
    with convert_internal_errors():
        if 'whiteboard' in data:
            new_whiteboard = data['whiteboard']
            if new_whiteboard != recipe.whiteboard:
                _record_activity(recipe, u'Whiteboard', recipe.whiteboard,
                                 new_whiteboard)
                recipe.whiteboard = new_whiteboard
        if 'reviewed' in data:
            recipe.set_reviewed_state(identity.current.user,
                                      bool(data['reviewed']))
    return jsonify(recipe.__json__())
Пример #13
0
def add_system():
    # We accept JSON or form-encoded for convenience
    if request.json:
        if 'fqdn' not in request.json:
            raise BadRequest400('Missing fqdn key')
        new_fqdn = request.json['fqdn']
    elif request.form:
        if 'fqdn' not in request.form:
            raise BadRequest400('Missing fqdn parameter')
        new_fqdn = request.form['fqdn']
    else:
        raise UnsupportedMediaType415
    with convert_internal_errors():
        if System.query.filter(System.fqdn == new_fqdn).count() != 0:
            raise Conflict409('System with fqdn %r already exists' % new_fqdn)
        system = System(fqdn=new_fqdn, owner=identity.current.user)
        session.add(system)
        # new systems are visible to everybody by default
        system.custom_access_policy = SystemAccessPolicy()
        system.custom_access_policy.add_rule(SystemPermission.view,
                everybody=True)
    # XXX this should be 201 with Location: /systems/FQDN/ but 302 is more 
    # convenient because it lets us use a traditional browser form without AJAX 
    # handling, and for now we're redirecting to /view/FQDN until that is moved 
    # to /systems/FQDN/
    return flask_redirect(url(u'/view/%s#essentials' % system.fqdn))
Пример #14
0
def _create_labcontroller_helper(data):
    with convert_internal_errors():
        if LabController.query.filter_by(fqdn=data['fqdn']).count():
            raise Conflict409('Lab Controller %s already exists' % data['fqdn'])

        user = find_user_or_create(data['user_name'])
        user = update_user(
            user=user,
            display_name=data['fqdn'],
            email_address=data.get('email_address', user.email_address),
            password=data.get('password', user.password)
        )
        labcontroller = LabController(fqdn=data['fqdn'], disabled=False)
        labcontroller.record_activity(
            user=identity.current.user, service=u'HTTP',
            action=u'Changed', field=u'FQDN', old=u'', new=data['fqdn'])

        labcontroller.user = user
        labcontroller.record_activity(
            user=identity.current.user, service=u'HTTP',
            action=u'Changed', field=u'User', old=u'', new=user.user_name)

        # For backwards compatibility
        labcontroller.record_activity(
            user=identity.current.user, service=u'HTTP',
            action=u'Changed', field=u'Disabled', old=u'', new=unicode(labcontroller.disabled))

        session.add(labcontroller)
        # flush it so we return an id, otherwise we'll end up back in here from
        # the edit form
        session.flush()

    response = jsonify(labcontroller.__json__())
    response.status_code = 201
    return response
Пример #15
0
def exclude_user(group_name):
    """
    Exclude a user from an inverted group. Then the user will not have the group
    membership.

    :param group_name: Group's name.
    :jsonparam string user_name: User's username.

    """
    u = identity.current.user
    data = read_json_request(request)
    group = _get_group_by_name(group_name, lockmode='update')
    if not group.can_modify_membership(identity.current.user):
        raise Forbidden403('Cannot edit membership of group %s' % group_name)
    if group.membership_type == GroupMembershipType.normal:
        raise NotFound404('Normal group %s do not have excluded users' % group_name)
    if 'user_name' not in data:
        raise BadRequest400('User not specified')
    user = _get_user_by_username(data['user_name'])
    if user in group.users:
        if not group.can_exclude_member(u, user.id):
            raise Forbidden403('Cannot exclude user %s from group %s' % (user, group_name))
        with convert_internal_errors():
            group.exclude_user(user, agent=identity.current.user)
    else:
        raise Conflict409('User %s is already excluded from group %s' %
                (user.user_name, group_name))
    return '', 204
Пример #16
0
def update_recipe(id):
    """
    Updates the attributes of a recipe. The request must be 
    :mimetype:`application/json`.

    :param id: Recipe's id.
    :jsonparam string whiteboard: Whiteboard of the recipe.
    :status 200: Recipe was updated.
    :status 400: Invalid data was given.
    """

    recipe = _get_recipe_by_id(id)
    if not recipe.can_edit(identity.current.user):
        raise Forbidden403('Cannot edit recipe %s' % recipe.id)
    data = read_json_request(request)
    with convert_internal_errors():
        if 'whiteboard' in data:
            new_whiteboard = data['whiteboard']
            if new_whiteboard != recipe.whiteboard:
                _record_activity(recipe, u'Whiteboard', recipe.whiteboard,
                    new_whiteboard)
                recipe.whiteboard = new_whiteboard
        if 'reviewed' in data:
            recipe.set_reviewed_state(identity.current.user, bool(data['reviewed']))
    return jsonify(recipe.__json__())
Пример #17
0
def update_reservation_request(id):
    """
    Updates the reservation request of a recipe. The request must be 
    :mimetype:`application/json`.

    :param id: Recipe's id.
    :jsonparam boolean reserve: Whether the system will be reserved at the end
      of the recipe. If true, the system will be reserved. If false, the system
      will not be reserved.
    :jsonparam int duration: Number of seconds to reserve the system.
    :jsonparam string when: Circumstances under which the system will be 
      reserved. Valid values are:

      onabort
        If the recipe status is Aborted.
      onfail
        If the recipe status is Aborted, or the result is Fail.
      onwarn
        If the recipe status is Aborted, or the result is Fail or Warn.
      always
        Unconditionally.
    """

    recipe = _get_recipe_by_id(id)
    if not recipe.can_update_reservation_request(identity.current.user):
        raise Forbidden403(
            'Cannot update the reservation request of recipe %s' % recipe.id)
    data = read_json_request(request)
    if 'reserve' not in data:
        raise BadRequest400('No reserve specified')
    with convert_internal_errors():
        if data['reserve']:
            if not recipe.reservation_request:
                recipe.reservation_request = RecipeReservationRequest()
            if 'duration' in data:
                duration = int(data['duration'])
                if duration > MAX_SECONDS_PROVISION:
                    raise BadRequest400(
                        'Reservation time exceeds maximum time of %s hours' %
                        MAX_HOURS_PROVISION)
                old_duration = recipe.reservation_request.duration
                recipe.reservation_request.duration = duration
                _record_activity(recipe, u'Reservation Request', old_duration,
                                 duration)
            if 'when' in data:
                old_condition = recipe.reservation_request.when
                new_condition = RecipeReservationCondition.from_string(
                    data['when'])
                recipe.reservation_request.when = new_condition
                _record_activity(recipe, u'Reservation Condition',
                                 old_condition, new_condition)
            session.flush()  # to ensure the id is populated
            return jsonify(recipe.reservation_request.__json__())
        else:
            if recipe.reservation_request:
                session.delete(recipe.reservation_request)
                _record_activity(recipe, u'Reservation Request',
                                 recipe.reservation_request.duration, None)
            return jsonify(RecipeReservationRequest.empty_json())
Пример #18
0
def _extend_watchdog(recipe_id, data):
    recipe = _get_recipe_by_id(recipe_id)
    kill_time = data.get('kill_time')
    if not kill_time:
        raise BadRequest400('Time not specified')
    with convert_internal_errors():
        seconds = recipe.extend(kill_time)
    return jsonify({'seconds': seconds})
Пример #19
0
def _extend_watchdog(recipe_id, data):
    recipe = _get_recipe_by_id(recipe_id)
    kill_time = data.get('kill_time')
    if not kill_time:
        raise BadRequest400('Time not specified')
    with convert_internal_errors():
        seconds = recipe.extend(kill_time)
    return jsonify({'seconds': seconds})
Пример #20
0
def create_group():
    """
    Creates a new user group in Beaker. The request must be 
    :mimetype:`application/json`.

    :jsonparam string group_name: Symbolic name for the group.
    :jsonparam string display_name: Human-friendly display name for the group.
    :jsonparam string description: Description of the group.
    :jsonparam string root_password: Optional root password for group jobs.
      If this is not set, group jobs will use the root password preferences of 
      the job submitter.
    :jsonparam string membership_type: Specifies how group membership is populated.
      Possible values are:

      * normal: Group is initially empty, members are explicitly added and removed by
        group owner.
      * ldap: Membership is populated from the LDAP group with the same group name.
      * inverted: Group contains all Beaker users *except* users who have been explicitly
        excluded by the group owner.

    :status 201: The group was successfully created.
    """
    user = identity.current.user
    data = read_json_request(request)
    if 'group_name' not in data:
        raise BadRequest400('Missing group_name key')
    if 'display_name' not in data:
        raise BadRequest400('Missing display_name key')
    # for backwards compatibility
    if data.pop('ldap', False):
        data['membership_type'] = 'ldap'
    try:
        Group.by_name(data['group_name'])
    except NoResultFound:
        pass
    else:
        raise Conflict409("Group '%s' already exists" % data['group_name'])
    with convert_internal_errors():
        group = Group.lazy_create(group_name=data['group_name'])
        group.display_name = data['display_name']
        group.description = data.get('description')
        group.root_password = data.get('root_password')
        session.add(group)
        group.record_activity(user=user,
                              service=u'HTTP',
                              field=u'Group',
                              action=u'Created')
        if data.get('membership_type'):
            group.membership_type = GroupMembershipType.from_string(
                data['membership_type'])
        if group.membership_type == GroupMembershipType.ldap:
            group.refresh_ldap_members()
        else:  # LDAP groups don't have any owners
            group.add_member(user, is_owner=True, agent=identity.current.user)
    response = jsonify(group.__json__())
    response.status_code = 201
    response.headers.add('Location', absolute_url(group.href))
    return response
Пример #21
0
def reserve(fqdn):
    """
    Reserves the system "manually".
    """
    system = _get_system_by_FQDN(fqdn)
    with convert_internal_errors():
        reservation = system.reserve_manually(service=u'HTTP',
                user=identity.current.user)
    return jsonify(reservation.__json__())
Пример #22
0
def get_recipeset_comments(id):
    """
    Returns a JSON collection of comments made on a recipe set.

    :param id: ID of the recipe set.
    """
    recipeset = _get_rs_by_id(id)
    with convert_internal_errors():
        return jsonify({'entries': recipeset.comments})
Пример #23
0
def update_group(group_name):
    """
    Updates attributes of an existing group. The request body must be a JSON
    object containing one or more of the following keys.

    :jsonparam string group_name: New name for the group.
    :jsonparam string display_name: Display name of the group.
    :jsonparam string description: Description of the group.
    :jsonparam string root_password: Optional password. Can be an empty string.
      If empty, group jobs will use the root password preferences of the job submitter.
    :jsonparam string membership_type: New membership type for the group.
      See `POST /groups/` for more information.

    :status 200: Group was updated.
    :status 400: Invalid data was given.
    """
    group = _get_group_by_name(group_name)
    if not group.can_edit(identity.current.user):
        raise Forbidden403('Cannot edit group')
    data = read_json_request(request)
    with convert_internal_errors():
        user = identity.current.user
        renamed = False
        if 'group_name' in data:
            new_name = data['group_name']
            if new_name != group.group_name:
                if Group.query.filter(Group.group_name == new_name).count():
                    raise Conflict409('Group %s already exists' % new_name)
                group.set_name(user, u'HTTP', new_name)
                renamed = True
        if 'display_name' in data:
            new_display_name = data['display_name']
            if new_display_name != group.display_name:
                group.set_display_name(user, u'HTTP', new_display_name)
        if 'description' in data:
            new_description = data['description']
            if new_description != group.description:
                group.set_description(user, u'HTTP', new_description)
        if 'root_password' in data:
            new_root_password = data['root_password']
            if new_root_password != group.root_password:
                group.set_root_password(user, u'HTTP', new_root_password)
        # for backwards compatibility
        if data.pop('ldap', False):
            data['membership_type'] = 'ldap'
        if 'membership_type' in data:
            new_type = GroupMembershipType.from_string(
                    data['membership_type'])
            if (new_type == GroupMembershipType.ldap and not
                group.can_edit_ldap(user)):
                raise BadRequest400('Cannot edit LDAP group %s' % group)
            if new_type != group.membership_type:
                group.membership_type = new_type
    response = jsonify(group.to_json())
    if renamed:
        response.headers.add('Location', absolute_url(group.href))
    return response
Пример #24
0
def update_group(group_name):
    """
    Updates attributes of an existing group. The request body must be a JSON
    object containing one or more of the following keys.

    :jsonparam string group_name: New name for the group.
    :jsonparam string display_name: Display name of the group.
    :jsonparam string description: Description of the group.
    :jsonparam string root_password: Optional password. Can be an empty string.
      If empty, group jobs will use the root password preferences of the job submitter.
    :jsonparam string membership_type: New membership type for the group.
      See `POST /groups/` for more information.

    :status 200: Group was updated.
    :status 400: Invalid data was given.
    """
    group = _get_group_by_name(group_name)
    if not group.can_edit(identity.current.user):
        raise Forbidden403('Cannot edit group')
    data = read_json_request(request)
    with convert_internal_errors():
        user = identity.current.user
        renamed = False
        if 'group_name' in data:
            new_name = data['group_name']
            if new_name != group.group_name:
                if Group.query.filter(Group.group_name == new_name).count():
                    raise Conflict409('Group %s already exists' % new_name)
                group.set_name(user, u'HTTP', new_name)
                renamed = True
        if 'display_name' in data:
            new_display_name = data['display_name']
            if new_display_name != group.display_name:
                group.set_display_name(user, u'HTTP', new_display_name)
        if 'description' in data:
            new_description = data['description']
            if new_description != group.description:
                group.set_description(user, u'HTTP', new_description)
        if 'root_password' in data:
            new_root_password = data['root_password']
            if new_root_password != group.root_password:
                group.set_root_password(user, u'HTTP', new_root_password)
        # for backwards compatibility
        if data.pop('ldap', False):
            data['membership_type'] = 'ldap'
        if 'membership_type' in data:
            new_type = GroupMembershipType.from_string(
                    data['membership_type'])
            if (new_type == GroupMembershipType.ldap and not
                group.can_edit_ldap(user)):
                raise BadRequest400('Cannot edit LDAP group %s' % group)
            if new_type != group.membership_type:
                group.membership_type = new_type
    response = jsonify(group.to_json())
    if renamed:
        response.headers.add('Location', absolute_url(group.href))
    return response
Пример #25
0
def update_reservation_request(id):
    """
    Updates the reservation request of a recipe. The request must be 
    :mimetype:`application/json`.

    :param id: Recipe's id.
    :jsonparam boolean reserve: Whether the system will be reserved at the end
      of the recipe. If true, the system will be reserved. If false, the system
      will not be reserved.
    :jsonparam int duration: Number of seconds to reserve the system.
    :jsonparam string when: Circumstances under which the system will be 
      reserved. Valid values are:

      onabort
        If the recipe status is Aborted.
      onfail
        If the recipe status is Aborted, or the result is Fail.
      onwarn
        If the recipe status is Aborted, or the result is Fail or Warn.
      always
        Unconditionally.
    """

    recipe = _get_recipe_by_id(id)
    if not recipe.can_update_reservation_request(identity.current.user):
        raise Forbidden403('Cannot update the reservation request of recipe %s'
                % recipe.id)
    data = read_json_request(request)
    if 'reserve' not in data:
        raise BadRequest400('No reserve specified')
    with convert_internal_errors():
        if data['reserve']:
            if not recipe.reservation_request:
                recipe.reservation_request = RecipeReservationRequest()
            if 'duration' in data:
                duration = int(data['duration'])
                if duration > MAX_SECONDS_PROVISION:
                    raise BadRequest400('Reservation time exceeds maximum time of %s hours'
                            % MAX_HOURS_PROVISION)
                old_duration = recipe.reservation_request.duration
                recipe.reservation_request.duration = duration
                _record_activity(recipe, u'Reservation Request', old_duration,
                        duration)
            if 'when' in data:
                old_condition = recipe.reservation_request.when
                new_condition = RecipeReservationCondition.from_string(data['when'])
                recipe.reservation_request.when = new_condition
                _record_activity(recipe, u'Reservation Condition',
                        old_condition, new_condition)
            session.flush() # to ensure the id is populated
            return jsonify(recipe.reservation_request.__json__())
        else:
            if recipe.reservation_request:
                session.delete(recipe.reservation_request)
                _record_activity(recipe, u'Reservation Request',
                        recipe.reservation_request.duration, None)
            return jsonify(RecipeReservationRequest.empty_json())
Пример #26
0
def create_pool():
    """
    Creates a new system pool in Beaker. The request must be 
    :mimetype:`application/x-www-form-urlencoded` or 
    :mimetype:`application/json`.

    :jsonparam string name: Name for the system pool.
    :jsonparam string description: Description of the system pool.
    :jsonparam object owner: JSON object containing a ``user_name`` key or
      ``group_name`` key identifying the owner for the system pool.

    :status 201: The system pool was successfully created.
    """
    owner = None
    description = None
    u = identity.current.user
    if request.json:
        if 'name' not in request.json:
            raise BadRequest400('Missing pool name key')
        new_name = request.json['name']
        if 'owner' in request.json:
            owner =  request.json['owner']
        if 'description' in request.json:
            description = request.json['description']
    elif request.form:
        if 'name' not in request.form:
            raise BadRequest400('Missing pool name parameter')
        new_name = request.form['name']
        if 'owner' in request.form:
            owner =  request.form['owner']
        if 'description' in request.form:
            description = request.form['description']
    else:
        raise UnsupportedMediaType415
    with convert_internal_errors():
        if SystemPool.query.filter(SystemPool.name == new_name).count() != 0:
            raise Conflict409('System pool with name %r already exists' % new_name)
        pool = SystemPool(name=new_name, description=description)
        session.add(pool)
        if owner:
            owner, owner_type = _get_owner(owner)
            if owner_type == 'user':
                pool.owning_user = owner
            else:
                pool.owning_group = owner
        else:
            pool.owning_user = u
        # new systems pool are visible to everybody by default
        pool.access_policy = SystemAccessPolicy()
        pool.access_policy.add_rule(SystemPermission.view, everybody=True)
        pool.record_activity(user=u, service=u'HTTP',
                     action=u'Created', field=u'Pool',
                     new=unicode(pool))
    response = jsonify(pool.__json__())
    response.status_code = 201
    response.headers.add('Location', absolute_url(pool.href))
    return response
Пример #27
0
def reserve(fqdn):
    """
    Reserves the system "manually".
    """
    system = _get_system_by_FQDN(fqdn)
    with convert_internal_errors():
        reservation = system.reserve_manually(service=u'HTTP',
                                              user=identity.current.user)
    return jsonify(reservation.__json__())
Пример #28
0
def update_pool(pool_name):
    """
    Updates attributes of an existing system pool. The request body must be a JSON 
    object containing one or more of the following keys.

    :param pool_name: System pool's name.
    :jsonparam string name: New name for the system pool.
    :jsonparam string description: Description of the system pool.
    :jsonparam object owner: JSON object containing a ``user_name`` key or
      ``group_name`` key identifying the new owner for the system pool.
    :status 200: System pool was updated.
    :status 400: Invalid data was given.
    """
    pool = _get_pool_by_name(pool_name)
    if not pool.can_edit(identity.current.user):
        raise Forbidden403('Cannot edit system pool')
    data = read_json_request(request)

    # helper for recording activity below
    def record_activity(field, old, new, action=u'Changed'):
        pool.record_activity(user=identity.current.user,
                             service=u'HTTP',
                             action=action,
                             field=field,
                             old=old,
                             new=new)

    with convert_internal_errors():
        renamed = False
        if 'name' in data:
            new_name = data['name']
            if new_name != pool.name:
                if SystemPool.query.filter(
                        SystemPool.name == new_name).count():
                    raise Conflict409('System pool %s already exists' %
                                      new_name)
                record_activity(u'Name', pool.name, new_name)
                pool.name = new_name
                renamed = True
        if 'description' in data:
            new_description = data['description']
            if new_description != pool.description:
                record_activity(u'Description', pool.description,
                                new_description)
                pool.description = new_description
        if 'owner' in data:
            new_owner, owner_type = _get_owner(data['owner'])
            if owner_type == 'user':
                pool.change_owner(user=new_owner)
            else:
                pool.change_owner(group=new_owner)

    response = jsonify(pool.__json__())
    if renamed:
        response.headers.add('Location', absolute_url(pool.href))
    return response
Пример #29
0
def create_group():
    """
    Creates a new user group in Beaker. The request must be 
    :mimetype:`application/json`.

    :jsonparam string group_name: Symbolic name for the group.
    :jsonparam string display_name: Human-friendly display name for the group.
    :jsonparam string description: Description of the group.
    :jsonparam string root_password: Optional root password for group jobs.
      If this is not set, group jobs will use the root password preferences of 
      the job submitter.
    :jsonparam string membership_type: Specifies how group membership is populated.
      Possible values are:

      * normal: Group is initially empty, members are explicitly added and removed by
        group owner.
      * ldap: Membership is populated from the LDAP group with the same group name.
      * inverted: Group contains all Beaker users *except* users who have been explicitly
        excluded by the group owner.

    :status 201: The group was successfully created.
    """
    user = identity.current.user
    data = read_json_request(request)
    if 'group_name' not in data:
        raise BadRequest400('Missing group_name key')
    if 'display_name' not in data:
        raise BadRequest400('Missing display_name key')
    # for backwards compatibility
    if data.pop('ldap', False):
        data['membership_type'] = 'ldap'
    try:
        Group.by_name(data['group_name'])
    except NoResultFound:
        pass
    else:
        raise Conflict409("Group '%s' already exists" % data['group_name'])
    with convert_internal_errors():
        group = Group.lazy_create(group_name=data['group_name'])
        group.display_name = data['display_name']
        group.description = data.get('description')
        group.root_password = data.get('root_password')
        session.add(group)
        group.record_activity(user=user, service=u'HTTP',
                field=u'Group', action=u'Created')
        if data.get('membership_type'):
            group.membership_type = GroupMembershipType.from_string(
                data['membership_type'])
        if group.membership_type == GroupMembershipType.ldap:
            group.refresh_ldap_members()
        else: # LDAP groups don't have any owners
            group.add_member(user, is_owner=True, agent=identity.current.user)
    response = jsonify(group.__json__())
    response.status_code = 201
    response.headers.add('Location', absolute_url(group.href))
    return response
Пример #30
0
def get_recipe_task_comments(recipeid, taskid):
    """
    Returns a JSON collection of comments made on a recipe task.

    :param recipeid: Recipe id.
    :param taskid: Recipe task id.
    """
    task = _get_recipe_task_by_id(recipeid, taskid)
    with convert_internal_errors():
        return jsonify({'entries': task.comments})
Пример #31
0
def get_recipe_task_comments(recipeid, taskid):
    """
    Returns a JSON collection of comments made on a recipe task.

    :param recipeid: Recipe id.
    :param taskid: Recipe task id.
    """
    task = _get_recipe_task_by_id(recipeid, taskid)
    with convert_internal_errors():
        return jsonify({'entries': task.comments})
Пример #32
0
def grant_loan(fqdn):
    """
    Lends the system to the specified user (or borrows the system for
    the current user if no other user is specified)

    """
    system = _get_system_by_FQDN(fqdn)
    data = read_json_request(request)
    recipient = data.get("recipient")
    comment = data.get("comment")
    with convert_internal_errors():
        system.grant_loan(recipient, comment, service=u'HTTP')
    return jsonify(system.get_loan_details())
Пример #33
0
def get_group_by_id_or_name():
    """
    Created for backwards compatibility. Will redirect to /groups/<group_name>.

    :queryparam group_id: Group's id.
    :queryparam group_name: Group's name.
    """
    if 'group_id' in request.args:
        with convert_internal_errors():
            group = Group.by_id(request.args['group_id'])
    elif 'group_name' in request.args:
        group = _get_group_by_name(request.args['group_name'])
    else:
        raise NotFound404
    return flask_redirect(absolute_url(group.href))
Пример #34
0
def get_group_by_id_or_name():
    """
    Created for backwards compatibility. Will redirect to /groups/<group_name>.

    :queryparam group_id: Group's id.
    :queryparam group_name: Group's name.
    """
    if 'group_id' in request.args:
        with convert_internal_errors():
            group = Group.by_id(request.args['group_id'])
    elif 'group_name' in request.args:
        group = _get_group_by_name(request.args['group_name'])
    else:
        raise NotFound404
    return flask_redirect(absolute_url(group.href))
Пример #35
0
def submit_inventory_job():
    """
    Submit a inventory job with the most suitable distro selected automatically.

    Returns a dictionary consisting of the job_id, recipe_id, status (recipe status) 
    and the job XML. If ``dryrun`` is set to ``True`` in the request, the first three 
    are set to ``None``.

    :jsonparam string fqdn: Fully-qualified domain name for the system.
    :jsonparam bool dryrun: If True, do not submit the job
    """
    if 'fqdn' not in request.json:
        raise BadRequest400('Missing the fqdn parameter')
    fqdn = request.json['fqdn']
    if 'dryrun' in request.json:
        dryrun = request.json['dryrun']
    else:
        dryrun = False
    try:
        system = System.by_fqdn(fqdn, identity.current.user)
    except NoResultFound:
        raise BadRequest400('System not found: %s' % fqdn)
    if system.find_current_hardware_scan_recipe():
        raise Conflict409('Hardware scanning already in progress')
    distro = system.distro_tree_for_inventory()
    if not distro:
        raise BadRequest400(
            'Could not find a compatible distro for hardware scanning available to this system'
        )
    job_details = {}
    job_details['system'] = system
    job_details['whiteboard'] = 'Update Inventory for %s' % fqdn
    with convert_internal_errors():
        job_xml = Job.inventory_system_job(distro,
                                           dryrun=dryrun,
                                           **job_details)
    r = {}
    if not dryrun:
        r = system.find_current_hardware_scan_recipe().__json__()
    else:
        r = {
            'recipe_id': None,
            'status': None,
            'job_id': None,
        }
    r['job_xml'] = job_xml
    r = jsonify(r)
    return r
Пример #36
0
def update_pool(pool_name):
    """
    Updates attributes of an existing system pool. The request body must be a JSON 
    object containing one or more of the following keys.

    :param pool_name: System pool's name.
    :jsonparam string name: New name for the system pool.
    :jsonparam string description: Description of the system pool.
    :jsonparam object owner: JSON object containing a ``user_name`` key or
      ``group_name`` key identifying the new owner for the system pool.
    :status 200: System pool was updated.
    :status 400: Invalid data was given.
    """
    pool = _get_pool_by_name(pool_name)
    if not pool.can_edit(identity.current.user):
        raise Forbidden403('Cannot edit system pool')
    data = read_json_request(request)
    # helper for recording activity below
    def record_activity(field, old, new, action=u'Changed'):
        pool.record_activity(user=identity.current.user, service=u'HTTP',
                action=action, field=field, old=old, new=new)
    with convert_internal_errors():
        renamed = False
        if 'name' in data:
            new_name = data['name']
            if new_name != pool.name:
                if SystemPool.query.filter(SystemPool.name == new_name).count():
                    raise Conflict409('System pool %s already exists' % new_name)
                record_activity(u'Name', pool.name, new_name)
                pool.name = new_name
                renamed = True
        if 'description' in data:
            new_description = data['description']
            if new_description != pool.description:
                record_activity(u'Description', pool.description, new_description)
                pool.description = new_description
        if 'owner' in data:
            new_owner, owner_type = _get_owner(data['owner'])
            if owner_type == 'user':
                pool.change_owner(user=new_owner)
            else:
                pool.change_owner(group=new_owner)

    response = jsonify(pool.__json__())
    if renamed:
        response.headers.add('Location', absolute_url(pool.href))
    return response
Пример #37
0
def update_reservation(fqdn):
    """
    Updates the system's current reservation. The only permitted update is to 
    end the reservation (returning the system), and this is only permitted when 
    the reservation was "manual" (not made by the scheduler).
    """
    system = _get_system_by_FQDN(fqdn)
    data = read_json_request(request)
    # This interprets both PATCH and PUT as PATCH
    finish_time = data.get('finish_time')
    with convert_internal_errors():
        if finish_time == "now":
            reservation = system.unreserve_manually_reserved(
                service=u'HTTP', user=identity.current.user)
        else:
            raise ValueError('Reservation durations are not configurable')
    return jsonify(reservation.__json__())
Пример #38
0
def update_loan(fqdn):
    """
    Updates a loan on the system with the given fully-qualified
    domain name.

    Currently, the only permitted update is to return it.
    """
    system = _get_system_by_FQDN(fqdn)
    data = read_json_request(request)
    # This interprets both PATCH and PUT as PATCH
    finish = data.get("finish")
    with convert_internal_errors():
        if finish == "now":
            system.return_loan(service=u'HTTP')
        else:
            raise ValueError("Loan durations are not yet configurable")
    return jsonify(system.get_loan_details())
Пример #39
0
def update_loan(fqdn):
    """
    Updates a loan on the system with the given fully-qualified
    domain name.

    Currently, the only permitted update is to return it.
    """
    system = _get_system_by_FQDN(fqdn)
    data = read_json_request(request)
    # This interprets both PATCH and PUT as PATCH
    finish = data.get("finish")
    with convert_internal_errors():
        if finish == "now":
            system.return_loan(service=u'HTTP')
        else:
            raise ValueError("Loan durations are not yet configurable")
    return jsonify(system.get_loan_details())
Пример #40
0
def update_reservation(fqdn):
    """
    Updates the system's current reservation. The only permitted update is to 
    end the reservation (returning the system), and this is only permitted when 
    the reservation was "manual" (not made by the scheduler).
    """
    system = _get_system_by_FQDN(fqdn)
    data = read_json_request(request)
    # This interprets both PATCH and PUT as PATCH
    finish_time = data.get('finish_time')
    with convert_internal_errors():
        if finish_time == "now":
            reservation = system.unreserve_manually_reserved(service=u'HTTP',
                    user=identity.current.user)
        else:
            raise ValueError('Reservation durations are not configurable')
    return jsonify(reservation.__json__())
Пример #41
0
def doit():
    distro_trees = []
    for id in request.form.getlist('distro_tree_id'):
        try:
            distro_trees.append(DistroTree.by_id(id))
        except NoResultFound:
            raise BadRequest400('Distro tree %r does not exist' % id)
    job_details = {}
    job_details['pick'] = request.form.get('pick') or 'auto'
    system_choice = 'any system'
    if job_details['pick'] == 'fqdn':
        try:
            job_details['system'] = System.by_fqdn(request.form.get('system'),
                                                   identity.current.user)
            system_choice = 'a specific system'
        except DatabaseLookupError:
            raise BadRequest400('System %s not found' %
                                request.form.get('system'))
    elif job_details['pick'] == 'lab':
        try:
            job_details['lab'] = LabController.by_name(request.form.get('lab'))
            system_choice = 'any lab system'
        except NoResultFound:
            raise BadRequest400('Lab controller %s not found' %
                                request.form.get('lab'))
    reservetime = int(
        request.form.get('reserve_duration') or DEFAULT_RESERVE_SECONDS)
    if reservetime > MAX_SECONDS_PROVISION:
        raise BadRequest400(
            'Reservation time exceeds maximum time of %s hours' %
            MAX_HOURS_PROVISION)
    job_details['reservetime'] = reservetime
    job_details['whiteboard'] = request.form.get('whiteboard')
    if not job_details['whiteboard']:
        job_details['whiteboard'] = (
            "Reserve Workflow provision of distro %s on %s for %d seconds" %
            (request.form.get('distro'), system_choice,
             job_details['reservetime']))

    job_details['ks_meta'] = request.form.get('ks_meta')
    job_details['koptions'] = request.form.get('koptions')
    job_details['koptions_post'] = request.form.get('koptions_post')
    with convert_internal_errors():
        job = Job.provision_system_job(distro_trees, **job_details)
    return 'Created %s' % job.t_id, 201, [('Location',
                                           absolute_url('/jobs/%s' % job.id))]
Пример #42
0
def create_powertype():
    """
    Creates a new power type. The request must be :mimetype:`application/json`.

    :jsonparam string name: Name for the power type.
    :status 201: The power type was successfully created.
    """
    data = read_json_request(request)
    with convert_internal_errors():
        if PowerType.query.filter_by(**data).count():
            raise Conflict409('Power type %s already exists' % data['name'])
        powertype = PowerType(**data)
        activity = Activity(identity.current.user, u'HTTP', u'Created', u'PowerType', powertype.name)
        session.add_all([powertype, activity])

    response = jsonify(powertype.__json__())
    response.status_code = 201
    return response
Пример #43
0
def create_powertype():
    """
    Creates a new power type. The request must be :mimetype:`application/json`.

    :jsonparam string name: Name for the power type.
    :status 201: The power type was successfully created.
    """
    data = read_json_request(request)
    with convert_internal_errors():
        if PowerType.query.filter_by(**data).count():
            raise Conflict409('Power type %s already exists' % data['name'])
        powertype = PowerType(**data)
        activity = Activity(identity.current.user, u'HTTP', u'Created',
                            u'PowerType', powertype.name)
        session.add_all([powertype, activity])

    response = jsonify(powertype.__json__())
    response.status_code = 201
    return response
Пример #44
0
def submit_inventory_job():
    """
    Submit a inventory job with the most suitable distro selected automatically.

    Returns a dictionary consisting of the job_id, recipe_id, status (recipe status) 
    and the job XML. If ``dryrun`` is set to ``True`` in the request, the first three 
    are set to ``None``.

    :jsonparam string fqdn: Fully-qualified domain name for the system.
    :jsonparam bool dryrun: If True, do not submit the job
    """
    if 'fqdn' not in request.json:
        raise BadRequest400('Missing the fqdn parameter')
    fqdn = request.json['fqdn']
    if 'dryrun' in request.json:
        dryrun = request.json['dryrun']
    else:
        dryrun = False
    try:
        system = System.by_fqdn(fqdn, identity.current.user)
    except NoResultFound:
        raise BadRequest400('System not found: %s' % fqdn)
    if system.find_current_hardware_scan_recipe():
        raise Conflict409('Hardware scanning already in progress')
    distro = system.distro_tree_for_inventory()
    if not distro:
        raise BadRequest400('Could not find a compatible distro for hardware scanning available to this system')
    job_details = {}
    job_details['system'] = system
    job_details['whiteboard'] = 'Update Inventory for %s' % fqdn
    with convert_internal_errors():
        job_xml = Job.inventory_system_job(distro, dryrun=dryrun, **job_details)
    r = {}
    if not dryrun:
        r = system.find_current_hardware_scan_recipe().__json__()
    else:
        r = {'recipe_id': None,
             'status': None,
             'job_id': None,
        }
    r['job_xml'] = job_xml
    r = jsonify(r)
    return r
Пример #45
0
def grant_loan(fqdn):
    """
    Lends the system to the specified user (or borrows the system for
    the current user if no other user is specified)

    """
    system = _get_system_by_FQDN(fqdn)
    data = read_json_request(request)
    recipient = data.get("recipient")
    if recipient is None:
        user_name = identity.current.user.user_name
    elif isinstance(recipient, basestring):
        user_name = recipient
    else:
        user_name = recipient.get('user_name')
    comment = data.get("comment")
    with convert_internal_errors():
        system.grant_loan(user_name, comment, service=u'HTTP')
    return jsonify(system.get_loan_details())
Пример #46
0
def update_reservation_request(id):
    """
    Updates the reservation request of a recipe. The request must be 
    :mimetype:`application/json`.

    :param id: Recipe's id.
    :jsonparam boolean reserve: Whether the system will be reserved at the end
      of the recipe. If true, the system will be reserved. If false, the system
      will not be reserved.
    :jsonparam int duration: Number of seconds to rerserve the system.
    """

    recipe = _get_recipe_by_id(id)
    if not recipe.can_update_reservation_request(identity.current.user):
        raise Forbidden403('Cannot update the reservation request of recipe %s'
                % recipe.id)
    data = read_json_request(request)
    if 'reserve' not in data:
        raise BadRequest400('No reserve specified')
    with convert_internal_errors():
        if data['reserve']:
            if 'duration' not in data:
                raise BadRequest400('No duration specified')
            duration = data['duration']
            if duration > MAX_SECONDS_PROVISION:
                raise BadRequest400('Reservation time exceeds maximum time of %s hours' % MAX_HOURS_PROVISION)
            if recipe.reservation_request:
                old_duration = recipe.reservation_request.duration
                recipe.reservation_request.duration = data['duration']
                _record_activity(recipe, u'Reservation Request', old_duration,
                        data['duration'])
            else:
                reservation_request = RecipeReservationRequest(data['duration'])
                recipe.reservation_request = reservation_request
                _record_activity(recipe, u'Reservation Request', None,
                        reservation_request.duration, 'Changed')
            return jsonify(recipe.reservation_request.__json__())
        else:
            if recipe.reservation_request:
                session.delete(recipe.reservation_request)
                _record_activity(recipe, u'Reservation Request',
                        recipe.reservation_request.duration, None)
            return jsonify(RecipeReservationRequest.empty_json())
Пример #47
0
def grant_loan(fqdn):
    """
    Lends the system to the specified user (or borrows the system for
    the current user if no other user is specified)

    """
    system = _get_system_by_FQDN(fqdn)
    data = read_json_request(request)
    recipient = data.get("recipient")
    if recipient is None:
        user_name = identity.current.user.user_name
    elif isinstance(recipient, basestring):
        user_name = recipient
    else:
        user_name = recipient.get('user_name')
    comment = data.get("comment")
    with convert_internal_errors():
        system.grant_loan(user_name, comment, service=u'HTTP')
    return jsonify(system.get_loan_details())
Пример #48
0
def post_recipeset_comment(id):
    """
    Adds a new comment to a recipe set. The request must be :mimetype:`application/json`.

    :param id: ID of the recipe set.
    :jsonparam string comment: Comment text.
    """
    recipeset = _get_rs_by_id(id)
    if not recipeset.can_comment(identity.current.user):
        raise Forbidden403('Cannot post recipe set comment')
    data = read_json_request(request)
    if 'comment' not in data:
        raise BadRequest400('Missing "comment" key')
    with convert_internal_errors():
        comment = RecipeSetComment(user=identity.current.user,
                                   comment=data['comment'])
        recipeset.comments.append(comment)
    session.flush()  # to populate the id
    return jsonify(comment.__json__())
Пример #49
0
def update_reservation_request(id):
    """
    Updates the reservation request of a recipe. The request must be 
    :mimetype:`application/json`.

    :param id: Recipe's id.
    :jsonparam boolean reserve: Whether the system will be reserved at the end
      of the recipe. If true, the system will be reserved. If false, the system
      will not be reserved.
    :jsonparam int duration: Number of seconds to rerserve the system.
    """

    recipe = _get_recipe_by_id(id)
    if not recipe.can_update_reservation_request(identity.current.user):
        raise Forbidden403('Cannot update the reservation request of recipe %s'
                % recipe.id)
    data = read_json_request(request)
    if 'reserve' not in data:
        raise BadRequest400('No reserve specified')
    with convert_internal_errors():
        if data['reserve']:
            if 'duration' not in data:
                raise BadRequest400('No duration specified')
            duration = data['duration']
            if duration > MAX_SECONDS_PROVISION:
                raise BadRequest400('Reservation time exceeds maximum time of %s hours' % MAX_HOURS_PROVISION)
            if recipe.reservation_request:
                old_duration = recipe.reservation_request.duration
                recipe.reservation_request.duration = data['duration']
                _record_activity(recipe, u'Reservation Request', old_duration,
                        data['duration'])
            else:
                reservation_request = RecipeReservationRequest(data['duration'])
                recipe.reservation_request = reservation_request
                _record_activity(recipe, u'Reservation Request', None,
                        reservation_request.duration, 'Changed')
            return jsonify(recipe.reservation_request.__json__())
        else:
            if recipe.reservation_request:
                session.delete(recipe.reservation_request)
                _record_activity(recipe, u'Reservation Request',
                        recipe.reservation_request.duration, None)
            return jsonify(RecipeReservationRequest.empty_json())
Пример #50
0
def _update_recipeset(recipeset, data=None):
    if not data:
        data = {}

    def record_activity(field, old=None, new=None, action=u'Changed'):
        recipeset.record_activity(user=identity.current.user,
                                  service=u'HTTP',
                                  action=action,
                                  field=field,
                                  old=old,
                                  new=new)

    with convert_internal_errors():
        if 'priority' in data:
            priority = TaskPriority.from_string(data['priority'])
            if priority != recipeset.priority:
                if not recipeset.can_change_priority(identity.current.user):
                    raise Forbidden403('Cannot change recipe set %s priority' %
                                       recipeset.id)
                allowed = recipeset.allowed_priorities(identity.current.user)
                if priority not in allowed:
                    raise Forbidden403(
                        'Cannot set recipe set %s priority to %s, '
                        'permitted priorities are: %s' %
                        (recipeset.id, priority, ' '.join(
                            unicode(pri) for pri in allowed)))
                record_activity(u'Priority',
                                old=recipeset.priority.value,
                                new=priority.value)
                recipeset.priority = priority
        if 'waived' in data:
            if not isinstance(data['waived'], bool):
                raise ValueError('waived key must be true or false')
            waived = data['waived']
            if waived != recipeset.waived:
                if not recipeset.can_waive(identity.current.user):
                    raise Forbidden403('Cannot waive recipe set %s' %
                                       recipeset.id)
                record_activity(u'Waived',
                                old=unicode(recipeset.waived),
                                new=unicode(waived))
                recipeset.waived = waived
Пример #51
0
def post_recipe_task_comment(recipeid, taskid):
    """
    Adds a new comment to a recipe task. The request must be :mimetype:`application/json`.

    :param recipeid: Recipe id.
    :param taskid: Recipe task id.
    :jsonparam string comment: Comment text.
    """
    task = _get_recipe_task_by_id(recipeid, taskid)
    if not task.can_comment(identity.current.user):
        raise Forbidden403('Cannot post recipe task comment')
    data = read_json_request(request)
    if 'comment' not in data:
        raise BadRequest400('Missing "comment" key')
    with convert_internal_errors():
        comment = RecipeTaskComment(user=identity.current.user,
                comment=data['comment'])
        task.comments.append(comment)
    session.flush() # to populate the id
    return jsonify(comment.__json__())
Пример #52
0
def create_user():
    """
    Creates a new user account in Beaker.
    """
    data = read_json_request(request)
    with convert_internal_errors():
        new_user_name = data.get('user_name', '').strip()
        existing_user = User.by_user_name(new_user_name)
        if existing_user is not None:
            raise Conflict409('User %s already exists' % new_user_name)
        new_display_name = data.get('display_name', '').strip()
        new_email_address = data.get('email_address', '').strip()
        user = User(user_name=new_user_name,
                    display_name=new_display_name,
                    email_address=new_email_address)
        session.add(user)
        session.flush()  # to populate id
    response = jsonify(user_full_json(user))
    response.status_code = 201
    response.headers.add('Location', absolute_url(user.href))
    return response
Пример #53
0
def create_user():
    """
    Creates a new user account in Beaker.
    """
    data = read_json_request(request)
    with convert_internal_errors():
        new_user_name = data.get('user_name', '').strip()
        existing_user = User.by_user_name(new_user_name)
        if existing_user is not None:
            raise Conflict409('User %s already exists' % new_user_name)
        new_display_name = data.get('display_name', '').strip()
        new_email_address = data.get('email_address', '').strip()
        user = User(user_name=new_user_name,
                    display_name=new_display_name,
                    email_address=new_email_address)
        session.add(user)
        session.flush() # to populate id
    response = jsonify(user_full_json(user))
    response.status_code = 201
    response.headers.add('Location', absolute_url(user.href))
    return response
Пример #54
0
def readd_user(group_name):
    """
    Re-add a user who has been excluded from the group.

    :param group_name: Group's name.
    :queryparam string user_name: User's username.

    """
    u = identity.current.user
    group = _get_group_by_name(group_name, lockmode='update')
    if not group.can_modify_membership(identity.current.user):
        raise Forbidden403('Cannot edit membership of group %s' % group_name)
    if 'user_name' not in request.args:
        raise MethodNotAllowed405
    user = _get_user_by_username(request.args['user_name'])
    if user not in group.users:
        with convert_internal_errors():
            group.readd_user(user, agent=identity.current.user)
    else:
        raise Conflict409('User %s is not excluded from group %s' %
                (user.user_name, group_name))
    return '', 204
Пример #55
0
def readd_user(group_name):
    """
    Re-add a user who has been excluded from the group.

    :param group_name: Group's name.
    :queryparam string user_name: User's username.

    """
    u = identity.current.user
    group = _get_group_by_name(group_name, lockmode='update')
    if not group.can_modify_membership(identity.current.user):
        raise Forbidden403('Cannot edit membership of group %s' % group_name)
    if 'user_name' not in request.args:
        raise MethodNotAllowed405
    user = _get_user_by_username(request.args['user_name'])
    if user not in group.users:
        with convert_internal_errors():
            group.readd_user(user, agent=identity.current.user)
    else:
        raise Conflict409('User %s is not excluded from group %s' %
                          (user.user_name, group_name))
    return '', 204
Пример #56
0
def provision_system(fqdn):
    system = _get_system_by_FQDN(fqdn)
    if not system.can_configure_netboot(identity.current.user):
        raise Forbidden403('Cannot provision system')
    data = read_json_request(request)
    with convert_internal_errors():
        if not data['distro_tree'] or 'id' not in data['distro_tree']:
            raise ValueError('No distro tree specified')
        distro_tree = DistroTree.by_id(data['distro_tree']['id'])
        user = identity.current.user
        if user.rootpw_expired:
            raise ValueError('Your root password has expired, you must '
                             'change or clear it in order to provision.')
            redirect(u"/view/%s" % system.fqdn)
        install_options = system.manual_provision_install_options(distro_tree)\
                .combined_with(InstallOptions.from_strings(data.get('ks_meta'),
                    data.get('koptions'), data.get('koptions_post')))
        if 'ks' not in install_options.kernel_options:
            kickstart = generate_kickstart(install_options,
                                           distro_tree=distro_tree,
                                           system=system,
                                           user=user)
            install_options.kernel_options['ks'] = kickstart.link
        system.configure_netboot(distro_tree,
                                 install_options.kernel_options_str,
                                 service=u'HTTP')
        system.record_activity(user=identity.current.user,
                               service=u'HTTP',
                               action=u'Provision',
                               field=u'Distro Tree',
                               new=unicode(distro_tree))
        if data.get('reboot'):
            system.action_power(action=u'reboot', service=u'HTTP')
    # in future "installations" will be a real thing in our model,
    # but for now we have nothing to return
    return 'Provisioned', 201
Пример #57
0
def update_system(fqdn):
    system = _get_system_by_FQDN(fqdn)
    if not system.can_edit(identity.current.user):
        raise Forbidden403('Cannot edit system')
    data = read_json_request(request)
    # helper for recording activity below
    def record_activity(field, old, new, action=u'Changed'):
        system.record_activity(user=identity.current.user, service=u'HTTP',
                action=action, field=field, old=old, new=new)
    with convert_internal_errors():
        # XXX what a nightmare... need to use a validation/conversion library, 
        # and maybe simplify/relocate the activity recording stuff somehow
        changed = False
        renamed = False
        if 'fqdn' in data:
            new_fqdn = data['fqdn'].lower()
            if new_fqdn != system.fqdn:
                if System.query.filter(System.fqdn == new_fqdn).count():
                    raise Conflict409('System %s already exists' % new_fqdn)
                record_activity(u'FQDN', system.fqdn, new_fqdn)
                system.fqdn = new_fqdn
                changed = True
                renamed = True
        if 'owner' in data and data['owner'].get('user_name') != system.owner.user_name:
            if not system.can_change_owner(identity.current.user):
                raise Forbidden403('Cannot change owner')
            new_owner = User.by_user_name(data['owner'].get('user_name'))
            if new_owner is None:
                raise BadRequest400('No such user %s' % data['owner'].get('user_name'))
            record_activity(u'Owner', system.owner, new_owner)
            system.owner = new_owner
            changed = True
        if 'status' in data:
            new_status = SystemStatus.from_string(data['status'])
            if new_status != system.status:
                record_activity(u'Status', system.status, new_status)
                system.status = new_status
                if not new_status.bad and system.status_reason:
                    # clear the status reason for "good" statuses
                    record_activity(u'Status Reason', system.status_reason, None)
                    system.status_reason = None
                changed = True
        if 'status_reason' in data:
            new_reason = data['status_reason'] or None
            if new_reason and not system.status.bad:
                raise ValueError('Cannot set status reason when status is %s'
                        % system.status)
            if new_reason != system.status_reason:
                record_activity(u'Status Reason', system.status_reason, new_reason)
                system.status_reason = new_reason
                changed = True
        if 'type' in data:
            new_type = SystemType.from_string(data['type'])
            if new_type != system.type:
                record_activity(u'Type', system.type, new_type)
                system.type = new_type
                changed = True
        if 'arches' in data:
            new_arches = [Arch.by_name(a) for a in (data['arches'] or [])]
            added_arches = set(new_arches).difference(system.arch)
            removed_arches = set(system.arch).difference(new_arches)
            if added_arches or removed_arches:
                for added_arch in added_arches:
                    record_activity(u'Arch', None, added_arch, u'Added')
                for removed_arch in removed_arches:
                    record_activity(u'Arch', removed_arch, None, u'Removed')
                system.arch[:] = new_arches
                changed = True
        if 'lab_controller_id' in data:
            if data['lab_controller_id']:
                new_lc = LabController.by_id(data['lab_controller_id'])
            else:
                new_lc = None
            if new_lc != system.lab_controller:
                if system.open_reservation is not None:
                    raise Conflict409('Unable to change lab controller while system '
                            'is in use (return the system first)')
                record_activity(u'Lab Controller', system.lab_controller, new_lc)
                system.lab_controller = new_lc
                changed = True
        # If we're given any power-related keys, need to ensure system.power exists
        if not system.power and set(['power_type', 'power_address', 'power_user',
                'power_password', 'power_id', 'power_quiescent_period'])\
                .intersection(data.keys()):
            system.power = Power()
        if 'power_type' in data:
            new_power_type = PowerType.by_name(data['power_type'])
            if new_power_type != system.power.power_type:
                if not system.power.power_type:
                    old_power_type = ''
                else:
                    old_power_type = system.power.power_type.name
                record_activity(u'power_type', old_power_type, new_power_type.name)
                system.power.power_type = new_power_type
                changed = True
        if 'power_address' in data:
            new_power_address = data['power_address']
            if not new_power_address:
                raise ValueError('Power address is required')
            if new_power_address != system.power.power_address:
                record_activity(u'power_address', system.power.power_address,
                        data['power_address'])
                system.power.power_address = new_power_address
                changed = True
        if 'power_user' in data:
            new_power_user = data['power_user'] or u''
            if new_power_user != (system.power.power_user or u''):
                record_activity(u'power_user', u'********', u'********')
                system.power.power_user = new_power_user
                changed = True
        if 'power_password' in data:
            new_power_password = data['power_password'] or u''
            if new_power_password != (system.power.power_passwd or u''):
                record_activity(u'power_passwd', u'********', u'********')
                system.power.power_passwd = new_power_password
                changed = True
        if 'power_id' in data:
            new_power_id = data['power_id'] or u''
            if new_power_id != (system.power.power_id or u''):
                record_activity(u'power_id', system.power.power_id, new_power_id)
                system.power.power_id = new_power_id
                changed = True
        if 'power_quiescent_period' in data:
            new_qp = int(data['power_quiescent_period'])
            if new_qp != system.power.power_quiescent_period:
                record_activity(u'power_quiescent_period',
                        system.power.power_quiescent_period, new_qp)
                system.power.power_quiescent_period = new_qp
                changed = True
        if 'release_action' in data:
            new_release_action = ReleaseAction.from_string(data['release_action'])
            if new_release_action != (system.release_action or ReleaseAction.power_off):
                record_activity(u'release_action',
                        (system.release_action or ReleaseAction.power_off),
                        new_release_action)
                system.release_action = new_release_action
                changed = True
        if 'reprovision_distro_tree' in data:
            if (not data['reprovision_distro_tree'] or
                    'id' not in data['reprovision_distro_tree']):
                new_rpdt = None
            else:
                new_rpdt = DistroTree.by_id(data['reprovision_distro_tree']['id'])
            if new_rpdt != system.reprovision_distro_tree:
                record_activity(u'reprovision_distro_tree',
                        unicode(system.reprovision_distro_tree),
                        unicode(new_rpdt))
                system.reprovision_distro_tree = new_rpdt
                changed = True
        if 'location' in data:
            new_location = data['location'] or None
            if new_location != system.location:
                record_activity(u'Location', system.location, new_location)
                system.location = new_location
                changed = True
        if 'lender' in data:
            new_lender = data['lender'] or None
            if new_lender != system.lender:
                record_activity(u'Lender', system.lender, new_lender)
                system.lender = new_lender
                changed = True
        if 'kernel_type' in data:
            new_kernel_type = KernelType.by_name(data['kernel_type'])
            if new_kernel_type != system.kernel_type:
                record_activity(u'Kernel Type', system.kernel_type, new_kernel_type)
                system.kernel_type = new_kernel_type
                changed = True
        if 'hypervisor' in data:
            if data['hypervisor']:
                new_hypervisor = Hypervisor.by_name(data['hypervisor'])
            else:
                new_hypervisor = None
            if new_hypervisor != system.hypervisor:
                record_activity(u'Hypervisor', system.hypervisor, new_hypervisor)
                system.hypervisor = new_hypervisor
                changed = True
        if 'vendor' in data:
            new_vendor = data['vendor'] or None
            if new_vendor != system.vendor:
                record_activity(u'Vendor', system.vendor, new_vendor)
                system.vendor = new_vendor
                changed = True
        if 'model' in data:
            new_model = data['model'] or None
            if new_model != system.model:
                record_activity(u'Model', system.model, new_model)
                system.model = new_model
                changed = True
        if 'serial_number' in data:
            new_serial_number = data['serial_number'] or None
            if new_serial_number != system.serial:
                record_activity(u'Serial Number', system.serial, new_serial_number)
                system.serial = new_serial_number
                changed = True
        if 'mac_address' in data:
            new_mac_address = data['mac_address'] or None
            if new_mac_address != system.mac_address:
                record_activity(u'MAC Address', system.mac_address, new_mac_address)
                system.mac_address = new_mac_address
                changed = True
        if 'memory' in data:
            new_memory = int(data['memory']) if data['memory'] else None
            if new_memory != system.memory:
                record_activity(u'Memory', system.memory, new_memory)
                system.memory = new_memory
                changed = True
        if 'numa_nodes' in data:
            new_numa_nodes = int(data['numa_nodes']) if data['numa_nodes'] else None
            if not system.numa:
                system.numa = Numa()
            if new_numa_nodes != system.numa.nodes:
                record_activity(u'NUMA/Nodes', system.numa.nodes, new_numa_nodes)
                system.numa.nodes = new_numa_nodes
                changed = True
        if changed:
            # XXX clear checksum!?
            system.date_modified = datetime.datetime.utcnow()
    response = jsonify(system.__json__())
    if renamed:
        response.headers.add('Location', url('/view/%s' % system.fqdn))
    return response
Пример #58
0
def update_user(username):
    """
    Updates a Beaker user account.

    :param username: The user's username.
    :jsonparam string user_name: New username. If the username is changed, the 
      response will include a Location header referring to the new URL for newly 
      renamed user resource.
    :jsonparam string display_name: New display name.
    :jsonparam string email_address: New email address.
    :jsonparam string password: New password. Only valid when Beaker is not 
      using external authentication for this account.
    :jsonparam string root_password: Root password to be set on systems 
      provisioned by Beaker.
    :jsonparam boolean use_old_job_page: True if the user has opted to use the
      old, deprecated pre-Beaker-23 job page.
    :jsonparam boolean notify_job_completion: True if the user receives
      notifications upon the completion of an owned job.
    :jsonparam boolean notify_broken_system: True if the user receives
      notifications upon a system being automatically marked broken.
    :jsonparam boolean notify_system_loan: True if the user receives
      notifications when their systems are loaned or loans are returned.
    :jsonparam boolean notify_group_membership: True if the user receives
      notifications of modifications to the groups the user belongs to.
    :jsonparam boolean notify_reservesys: True if the user receives
      notifications upon reservesys being ready.
    :jsonparam boolean disabled: Whether the user should be temporarily 
      disabled. Disabled users cannot log in or submit jobs, and any running jobs 
      are cancelled when their account is disabled.
    :jsonparam string removed: Pass the string 'now' to remove a user account. 
      Pass null to un-remove a removed user account.
    """
    user = _get_user(username)
    data = read_json_request(request)
    renamed = False
    if data.get('password') is not None:
        if not user.can_change_password(identity.current.user):
            raise Forbidden403('Cannot change password for user %s' % user)
        with convert_internal_errors():
            user.password = data.pop('password')
    if data:
        if not user.can_edit(identity.current.user):
            raise Forbidden403('Cannot edit user %s' % user)
        with convert_internal_errors():
            if 'user_name' in data:
                new_user_name = data['user_name'].strip()
                if user.user_name != new_user_name:
                    if not user.can_rename(identity.current.user):
                        raise Forbidden403('Cannot rename user %s to %s'
                                % (user, new_user_name))
                    if User.by_user_name(new_user_name) is not None:
                        raise Conflict409('User %s already exists' % new_user_name)
                    user.user_name = new_user_name
                    renamed = True
            if 'display_name' in data:
                user.display_name = data['display_name'].strip()
            if 'email_address' in data:
                user.email_address = data['email_address'].strip()
            if 'root_password' in data:
                new_root_password = data['root_password']
                if user.root_password != new_root_password:
                    user.root_password = new_root_password
            if 'use_old_job_page' in data:
                user.use_old_job_page = data['use_old_job_page']
            if 'notify_job_completion' in data:
                user.notify_job_completion = data['notify_job_completion']
            if 'notify_broken_system' in data:
                user.notify_broken_system = data['notify_broken_system']
            if 'notify_system_loan' in data:
                user.notify_system_loan = data['notify_system_loan']
            if 'notify_group_membership' in data:
                user.notify_group_membership = data['notify_group_membership']
            if 'notify_reservesys' in data:
                user.notify_reservesys = data['notify_reservesys']
            if 'disabled' in data:
                user.disabled = data['disabled']
                if user.disabled:
                    _disable(user, method=u'HTTP')
            if 'removed' in data:
                if data['removed'] is None:
                    _unremove(user)
                elif data['removed'] == 'now':
                    _remove(user, method=u'HTTP')
                else:
                    raise ValueError('"removed" value must be "now" or null')
    session.flush()
    response = jsonify(user_full_json(user))
    if renamed:
        response.headers.add('Location', absolute_url(user.href))
    return response
Пример #59
0
def _extend_watchdog(recipe_id, data):
    recipe = _get_recipe_by_id(recipe_id)
    kill_time = data.get('kill_time')
    with convert_internal_errors():
        seconds = recipe.extend(kill_time)
    return jsonify({'seconds': seconds})
Пример #60
0
def update_labcontroller(fqdn):
    """
    Updates attributes of the lab controller identified by it's FQDN. The
    request body must be a json object or only the FQDN if
    that is the only value to be updated.

    :param string fqdn: Lab controller's new fully-qualified domain name.
    :jsonparam string user_name: User name associated with the lab controller.
    :jsonparam string email_address: Email of the user account associated with the lab controller.
    :jsonparam string password: Optional password for the user account used to login.
    :jsonparam string removed: If True, detaches all systems, cancels all
        running recipes and removes associated distro trees. If False, restores
        the lab controller.
    :jsonparam bool disabled: Whether the lab controller should be disabled. New
        recipes are not scheduled on a lab controller while it is disabled.
    :status 200: LabController updated.
    :status 400: Invalid data was given.
    """
    labcontroller = find_labcontroller_or_raise404(fqdn)
    if not labcontroller.can_edit(identity.current.user):
        raise Forbidden403('Cannot edit lab controller')
    data = read_json_request(request)
    with convert_internal_errors():
        # should the lab controller be removed?
        if data.get('removed', False) and not labcontroller.removed:
            remove_labcontroller(labcontroller)

        # should the controller be restored?
        if data.get('removed') is False and labcontroller.removed:
            restore_labcontroller(labcontroller)
        fqdn_changed = False
        new_fqdn = data.get('fqdn', fqdn)
        if labcontroller.fqdn != new_fqdn:
            lc = None
            try:
                lc = LabController.by_name(new_fqdn)
            except NoResultFound:
                pass
            if lc is not None:
                raise BadRequest400('FQDN %s already in use' % new_fqdn)

            labcontroller.record_activity(
                user=identity.current.user, service=u'HTTP',
                field=u'fqdn', action=u'Changed', old=labcontroller.fqdn, new=new_fqdn)
            labcontroller.fqdn = new_fqdn
            labcontroller.user.display_name = new_fqdn
            fqdn_changed = True
        if 'user_name' in data:
            user = find_user_or_create(data['user_name'])
            if labcontroller.user != user:
                user = update_user(
                    user,
                    display_name=new_fqdn,
                    email_address=data.get('email_address', user.email_address),
                    password=data.get('password', user.password)
                )
                labcontroller.record_activity(
                    user=identity.current.user, service=u'HTTP',
                    field=u'User', action=u'Changed',
                    old=labcontroller.user.user_name, new=user.user_name)
                labcontroller.user = user
        if 'email_address' in data:
            new_email_address = data.get('email_address')
            if labcontroller.user.email_address != new_email_address:
                labcontroller.user.email_address = new_email_address
        if data.get('password') is not None:
            labcontroller.user.password = data.get('password')
        if labcontroller.disabled != data.get('disabled', labcontroller.disabled):
            labcontroller.record_activity(
                user=identity.current.user, service=u'HTTP',
                field=u'disabled', action=u'Changed',
                old=unicode(labcontroller.disabled), new=data['disabled'])
            labcontroller.disabled = data['disabled']

    response = jsonify(labcontroller.__json__())
    if fqdn_changed:
        response.headers.add('Location', absolute_url(labcontroller.href))
    return response