예제 #1
0
class Preferences(RPCRoot):

    exposed = True

    # XMLRPC interface
    @expose()
    @identity.require(identity.not_anonymous())
    def remove_submission_delegate_by_name(self,
                                           delegate_name,
                                           service=u'XMLRPC'):
        user = identity.current.user
        try:
            submission_delegate = User.by_user_name(delegate_name)
        except NoResultFound:
            raise BX(_(u'%s is not a valid user name' % delegate_name))
        try:
            user.remove_submission_delegate(submission_delegate,
                                            service=service)
        except ValueError:
            raise BX(_(u'%s is not a submission delegate of %s' % \
                (delegate_name, user)))
        return delegate_name

    # XMLRPC Interface
    @expose()
    @identity.require(identity.not_anonymous())
    def add_submission_delegate_by_name(self,
                                        new_delegate_name,
                                        service=u'XMLRPC'):
        user = identity.current.user
        new_delegate = User.by_user_name(new_delegate_name)
        if not new_delegate:
            raise BX(_(u'%s is not a valid user' % new_delegate_name))
        user.add_submission_delegate(new_delegate, service)
        return new_delegate_name

    #XMLRPC method for updating user preferences
    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    @validate(validators=dict(email_address=validators.Email()))
    def update(self, email_address=None, tg_errors=None):
        """
        Update user preferences

        :param email_address: email address
        :type email_address: string
        """
        if tg_errors:
            raise BeakerException(', '.join(
                str(item) for item in tg_errors.values()))
        if email_address:
            if email_address == identity.current.user.email_address:
                raise BeakerException(
                    "Email address not changed: new address is same as before")
            else:
                identity.current.user.email_address = email_address
예제 #2
0
class SystemAction(object):
    @expose(format='json')
    @identity.require(identity.not_anonymous())
    @validate(validators={
        'system': CheckSystemValid(),
        'recipe_id': CheckRecipeValid(),
    })
    def report_system_problem(self,
                              system,
                              description,
                              recipe_id=None,
                              tg_errors=None,
                              **kw):
        if tg_errors:
            raise HTTPError(status=400, message=tg_errors)
        # CheckRecipeValid has converted the id into an ORM object
        if recipe_id is not None:
            recipe = recipe_id
        else:
            recipe = None
        mail.system_problem_report(system, description, recipe,
                                   identity.current.user)
        system.record_activity(user=identity.current.user,
                               service=u'WEBUI',
                               action=u'Reported problem',
                               field=u'Status',
                               old=None,
                               new=description)
        return {}
예제 #3
0
class RecipeSets(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True

    hidden_id = widgets.HiddenField(name='id')
    confirm = widgets.Label(name='confirm',
                            default="Are you sure you want to cancel?")
    message = widgets.TextArea(name='msg',
                               label=_(u'Reason?'),
                               help_text=_(u'Optional'))
    cancel_form = widgets.TableForm('cancel_recipeset',
                                    fields=[hidden_id, message, confirm],
                                    action='really_cancel',
                                    submit_text=_(u'Yes'))

    @identity.require(identity.not_anonymous())
    @expose(template="bkr.server.templates.form")
    def cancel(self, id):
        """
        Confirm cancel recipeset
        """
        try:
            recipeset = RecipeSet.by_id(id)
        except InvalidRequestError:
            flash(_(u"Invalid recipeset id %s" % id))
            redirect("/jobs/%s" % recipeset.job.id)
        if not recipeset.can_cancel(identity.current.user):
            flash(
                _(u"You don't have permission to cancel recipeset id %s" % id))
            redirect("/jobs/%s" % recipeset.job.id)
        return dict(
            title='Cancel RecipeSet %s' % id,
            form=self.cancel_form,
            action='./really_cancel',
            options={},
            value=dict(id=recipeset.id,
                       confirm='really cancel recipeset %s?' % id),
        )

    @identity.require(identity.not_anonymous())
    @expose()
    def really_cancel(self, id, msg=None):
        """
        Confirm cancel recipeset
        """
        try:
            recipeset = RecipeSet.by_id(id)
        except InvalidRequestError:
            flash(_(u"Invalid recipeset id %s" % id))
            redirect("/jobs/%s" % recipeset.job.id)
        if not recipeset.can_cancel(identity.current.user):
            flash(
                _(u"You don't have permission to cancel recipeset id %s" % id))
            redirect("/jobs/%s" % recipeset.job.id)
        recipeset.cancel(msg)
        recipeset.record_activity(user=identity.current.user,
                                  service=u'WEBUI',
                                  field=u'Status',
                                  action=u'Cancelled',
                                  old='',
                                  new='')
        flash(_(u"Successfully cancelled recipeset %s" % id))
        redirect("/jobs/%s" % recipeset.job.id)

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def stop(self, recipeset_id, stop_type, msg=None):
        """
        Set recipeset status to Completed
        """
        try:
            recipeset = RecipeSet.by_id(recipeset_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipeset ID: %s' % recipeset_id))
        if stop_type not in recipeset.stop_types:
            raise BX(
                _('Invalid stop_type: %s, must be one of %s' %
                  (stop_type, recipeset.stop_types)))
        kwargs = dict(msg=msg)
        return getattr(recipeset, stop_type)(**kwargs)
예제 #4
0
파일: group.py 프로젝트: joyxu/beaker
class Groups(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True

    def revoke_owner(self, group_id=None, id=None, **kw):

        if group_id is not None and id is not None:
            group = Group.by_id(group_id)
            user = User.by_id(id)
            service = 'WEBUI'
        else:
            group = Group.by_name(kw['group_name'])
            user = User.by_user_name(kw['member_name'])
            service = 'XMLRPC'

        if group.membership_type == GroupMembershipType.ldap:
            raise GroupOwnerModificationForbidden(
                'An LDAP group does not have an owner')

        if not group.can_edit(identity.current.user):
            raise GroupOwnerModificationForbidden(
                'You are not an owner of group %s' % group)

        if user not in group.users:
            raise GroupOwnerModificationForbidden('User is not a group member')

        if len(group.owners()) == 1 and not identity.current.user.is_admin():
            raise GroupOwnerModificationForbidden(
                'Cannot remove the only owner')
        else:
            group.revoke_ownership(user=user,
                                   agent=identity.current.user,
                                   service=service)
            # hack to return the user removing this owner
            # so that if the user was logged in as a group
            # owner, he/she can be redirected appropriately
            return str(identity.current.user.user_id)

    #XML-RPC interface
    @identity.require(identity.not_anonymous())
    @expose(format='json')
    def revoke_ownership(self, group, kw):
        """
        Revoke group ownership from an existing group member

        :param group: An existing group name
        :type group: string

        The *kw* argument must be an XML-RPC structure (dict)
        specifying the following keys:

            'member_name'
                 Group member's username to revoke ownership
        """

        return self.revoke_owner(group_name=group,
                                 member_name=kw['member_name'])

    def grant_owner(self, group_id=None, id=None, **kw):

        if group_id is not None and id is not None:
            group = Group.by_id(group_id)
            user = User.by_id(id)
            service = 'WEBUI'
        else:
            group = Group.by_name(kw['group_name'])
            user = User.by_user_name(kw['member_name'])
            service = 'XMLRPC'

        if group.membership_type == GroupMembershipType.ldap:
            raise GroupOwnerModificationForbidden(
                'An LDAP group does not have an owner')

        if not group.can_edit(identity.current.user):
            raise GroupOwnerModificationForbidden(
                'You are not an owner of the group %s' % group)

        if user not in group.users:
            raise GroupOwnerModificationForbidden('User is not a group member')
        else:
            group.grant_ownership(user=user,
                                  agent=identity.current.user,
                                  service=service)
            return ''

    #XML-RPC interface
    @identity.require(identity.not_anonymous())
    @expose(format='json')
    def grant_ownership(self, group, kw):
        """
        Grant group ownership to an existing group member

        :param group: An existing group name
        :type group: string

        The *kw* argument must be an XML-RPC structure (dict)
        specifying the following keys:

            'member_name'
                 Group member's username to grant ownership
        """
        return self.grant_owner(group_name=group,
                                member_name=kw['member_name'])

    # XML-RPC method for creating a group
    @identity.require(identity.not_anonymous())
    @expose(format='json')
    def create(self, kw):
        """
        Creates a new group.

        The *kw* argument must be an XML-RPC structure (dict)
        specifying the following keys:

            'group_name'
                 Group name (maximum 16 characters)
            'display_name'
                 Group display name
            'description'
                 Group description
            'ldap'
                 Populate users from LDAP (True/False)

        Returns a message whether the group was successfully created or
        raises an exception on failure.

        """
        display_name = kw.get('display_name')
        group_name = kw.get('group_name')
        description = kw.get('description')
        ldap = kw.get('ldap')
        password = kw.get('root_password')

        if ldap and not identity.current.user.is_admin():
            raise BX(_(u'Only admins can create LDAP groups'))
        if ldap and not config.get("identity.ldap.enabled", False):
            raise BX(_(u'LDAP is not enabled'))
        try:
            group = Group.by_name(group_name)
        except NoResultFound:
            group = Group()
            session.add(group)
            group.record_activity(user=identity.current.user,
                                  service=u'XMLRPC',
                                  field=u'Group',
                                  action=u'Created')
            group.display_name = display_name
            group.group_name = group_name
            group.description = description
            group.root_password = password
            if ldap:
                group.membership_type = GroupMembershipType.ldap
                group.refresh_ldap_members()
            else:
                group.add_member(identity.current.user,
                                 is_owner=True,
                                 service=u'XMLRPC',
                                 agent=identity.current.user)
            return 'Group created: %s.' % group_name
        else:
            raise BX(_(u'Group already exists: %s.' % group_name))

    # XML-RPC method for modifying a group
    @identity.require(identity.not_anonymous())
    @expose(format='json')
    def modify(self, group_name, kw):
        """
        Modifies an existing group. You must be an owner of a group to modify any details.

        :param group_name: An existing group name
        :type group_name: string

        The *kw* argument must be an XML-RPC structure (dict)
        specifying the following keys:

            'group_name'
                 New group name (maximum 16 characters)
            'display_name'
                 New group display name
            'add_member'
                 Add user (username) to the group
            'remove_member'
                 Remove an existing user (username) from the group
            'root_password'
                 Change the root password of this group.

        Returns a message whether the group was successfully modified or
        raises an exception on failure.

        """
        # if not called from the bkr group-modify
        if not kw:
            raise BX(_('Please specify an attribute to modify.'))

        try:
            group = Group.by_name(group_name)
        except NoResultFound:
            raise BX(_(u'Group does not exist: %s.' % group_name))

        if group.membership_type == GroupMembershipType.ldap:
            if not identity.current.user.is_admin():
                raise BX(_(u'Only admins can modify LDAP groups'))
            if kw.get('add_member', None) or kw.get('remove_member', None):
                raise BX(_(u'Cannot edit membership of an LDAP group'))

        user = identity.current.user
        if not group.can_edit(user):
            raise BX(_('You are not an owner of group %s' % group_name))

        group_name = kw.get('group_name', None)
        if group_name:
            try:
                Group.by_name(group_name)
            except NoResultFound:
                pass
            else:
                if group_name != group.group_name:
                    raise BX(
                        _(u'Failed to update group %s: Group name already exists: %s'
                          % (group.group_name, group_name)))

            group.set_name(user, u'XMLRPC', kw.get('group_name', None))

        display_name = kw.get('display_name', None)
        if display_name:
            group.set_display_name(user, u'XMLRPC', display_name)

        root_password = kw.get('root_password', None)
        if root_password:
            group.set_root_password(user, u'XMLRPC', root_password)

        if kw.get('add_member', None):
            username = kw.get('add_member')
            user = User.by_user_name(username)
            if user is None:
                raise BX(_(u'User does not exist %s' % username))
            if user.removed:
                raise BX(
                    _(u'Cannot add deleted user %s to group' % user.user_name))

            if user not in group.users:
                group.add_member(user,
                                 service=u'XMLRPC',
                                 agent=identity.current.user)
                mail.group_membership_notify(user,
                                             group,
                                             agent=identity.current.user,
                                             action='Added')
            else:
                raise BX(
                    _(u'User %s is already in group %s' %
                      (username, group.group_name)))

        if kw.get('remove_member', None):
            username = kw.get('remove_member')
            user = User.by_user_name(username)

            if user is None:
                raise BX(_(u'User does not exist %s' % username))

            if user not in group.users:
                raise BX(
                    _(u'No user %s in group %s' %
                      (username, group.group_name)))
            else:
                if not group.can_remove_member(identity.current.user,
                                               user.user_id):
                    raise BX(_(u'Cannot remove member'))

                groupUsers = group.users
                for usr in groupUsers:
                    if usr.user_id == user.user_id:
                        group.remove_member(user,
                                            service=u'XMLRPC',
                                            agent=identity.current.user)
                        removed = user
                        mail.group_membership_notify(
                            user,
                            group,
                            agent=identity.current.user,
                            action='Removed')
                        break

        #dummy success return value
        return ['1']

    # XML-RPC method for listing a group's members
    @expose(format='json')
    def members(self, group_name):
        """
        List the members of an existing group.

        :param group_name: An existing group name
        :type group_name: string

        Returns a list of the members (a dictionary containing each
        member's username, email, and whether the member is an owner
        or not).

        """
        try:
            group = Group.by_name(group_name)
        except NoResultFound:
            raise BX(_(u'Group does not exist: %s.' % group_name))

        users = []
        for u in group.users:
            user = {}
            user['username'] = u.user_name
            user['email'] = u.email_address
            if group.has_owner(u):
                user['owner'] = True
            else:
                user['owner'] = False
            users.append(user)

        return users
예제 #5
0
class RecipeTasks(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True

    def _warn_once(self, recipetask, msg):
        """
        Records a Warn result with the given message against the given recipe 
        task, but only if the same message does not already appear anywhere 
        else in the recipe already.
        """
        # We use a query with count() here, rather than loading all task
        # results, for efficiency. Bear in mind that this code may be
        # firing once we already have a very large number of results against
        # the task, which could be expensive to fully load.
        if RecipeTaskResult.query.join(RecipeTaskResult.recipetask)\
                .filter(RecipeTask.recipe_id == recipetask.recipe_id)\
                .filter(RecipeTaskResult.log == msg)\
                .with_entities(func.count(RecipeTaskResult.id)).scalar() == 0:
            recipetask.warn(u'/', 0, msg)
            # Need to explicitly commit here because the caller will be raising
            # an exception, which would otherwise roll everything back.
            session.commit()

    def _check_log_limit(self, recipetask):
        max_logs = config.get('beaker.max_logs_per_recipe', 7500)
        if not max_logs or max_logs <= 0:
            return
        task_log_count = LogRecipeTask.query.join(LogRecipeTask.parent)\
                .filter(RecipeTask.recipe_id == recipetask.recipe_id).count()
        result_log_count = LogRecipeTaskResult.query\
                .join(LogRecipeTaskResult.parent, RecipeTaskResult.recipetask)\
                .filter(RecipeTask.recipe_id == recipetask.recipe_id).count()
        if (task_log_count + result_log_count) >= max_logs:
            self._warn_once(recipetask, u'Too many logs in recipe')
            raise ValueError('Too many logs in recipe %s' %
                             recipetask.recipe_id)

    def _check_result_limit(self, recipetask):
        max_results_per_recipe = config.get('beaker.max_results_per_recipe',
                                            7500)
        if not max_results_per_recipe or max_results_per_recipe <= 0:
            return
        result_count = RecipeTaskResult.query.join(RecipeTaskResult.recipetask)\
                .filter(RecipeTask.recipe_id == recipetask.recipe_id).count()
        if result_count >= max_results_per_recipe:
            self._warn_once(recipetask, u'Too many results in recipe')
            raise ValueError(u'Too many results in recipe %s' %
                             recipetask.recipe_id)

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def register_file(self, server, task_id, path, filename, basepath):
        """
        register file and return path to store
        """
        try:
            recipetask = RecipeTask.by_id(task_id, lockmode='update')
        except NoResultFound:
            raise BX(_('Invalid task ID: %s' % task_id))
        Recipe.by_id(recipetask.recipe_id, lockmode='update')
        if recipetask.is_finished():
            raise BX('Cannot register file for finished task %s' %
                     recipetask.t_id)
        self._check_log_limit(recipetask)

        # Add the log to the DB if it hasn't been recorded yet.
        log_recipe = LogRecipeTask.lazy_create(
            recipe_task_id=recipetask.id,
            path=path,
            filename=filename,
        )
        log_recipe.server = server
        log_recipe.basepath = basepath
        recipetask.recipe.log_server = urlparse.urlparse(server)[1]
        return '%s' % recipetask.filepath

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def register_result_file(self, server, result_id, path, filename,
                             basepath):
        """
        register file and return path to store
        """
        try:
            result = RecipeTaskResult.by_id(result_id, lockmode='update')
        except NoResultFound:
            raise BX(_('Invalid result ID: %s' % result_id))
        RecipeTask.by_id(result.recipe_task_id, lockmode='update')
        Recipe.by_id(result.recipetask.recipe_id, lockmode='update')
        if result.recipetask.is_finished():
            raise BX('Cannot register file for finished task %s' %
                     result.recipetask.t_id)
        self._check_log_limit(result.recipetask)

        log_recipe = LogRecipeTaskResult.lazy_create(
            recipe_task_result_id=result.id,
            path=path,
            filename=filename,
        )
        log_recipe.server = server
        log_recipe.basepath = basepath
        result.recipetask.recipe.log_server = urlparse.urlparse(server)[1]
        return '%s' % result.filepath

    @cherrypy.expose
    def watchdogs(self, status='active', lc=None):
        """ Return all active/expired tasks for this lab controller
            The lab controllers login with host/fqdn
        """
        # TODO work on logic that determines whether or not originator
        # was qpid or kobo ?
        if lc is None:
            try:
                labcontroller = identity.current.user.lab_controller
            except AttributeError:
                raise BX(
                    _('No lab controller passed in and not currently logged in'
                      ))

            if not labcontroller:
                raise BX(
                    _(u'Invalid login: %s, must log in as a lab controller' %
                      identity.current.user))
        else:
            try:
                labcontroller = LabController.by_name(lc)
            except InvalidRequestError:
                raise BX(_(u'Invalid lab controller: %s' % lc))

        return [
            dict(recipe_id=w.recipe.id,
                 system=w.recipe.resource.fqdn,
                 is_virt_recipe=(w.recipe.resource.type == ResourceType.virt))
            for w in Watchdog.by_status(labcontroller, status)
        ]

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def start(self, task_id, watchdog_override=None):
        """
        Set task status to Running
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        return task.start(watchdog_override)

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def extend(self, task_id, kill_time):
        """
        Extend tasks watchdog by kill_time seconds
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        return task.extend(kill_time)

    @cherrypy.expose
    def watchdog(self, task_id):
        """
        Returns number of seconds left on task_id watchdog, or False if it doesn't exist.
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        return task.status_watchdog()

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def stop(self, task_id, stop_type, msg=None):
        """
        Set task status to Completed
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        if stop_type not in task.stop_types:
            raise BX(
                _('Invalid stop_type: %s, must be one of %s' %
                  (stop_type, task.stop_types)))
        kwargs = dict(msg=msg)
        return getattr(task, stop_type)(**kwargs)

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def update(self, task_id, data):
        """
        XML-RPC method used by the lab controller harness API to update 
        a recipe-task's attributes.
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        if 'name' in data:
            task.name = data['name']
        if 'version' in data:
            task.version = data['version']
        return {
            'id': task.id,
            'name': task.name,
            'version': task.version,
            'status': unicode(task.status)
        }

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def result(self,
               task_id,
               result_type,
               path=None,
               score=None,
               summary=None):
        """
        Record a Result
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        if result_type not in task.result_types:
            raise BX(
                _('Invalid result_type: %s, must be one of %s' %
                  (result_type, task.result_types)))
        self._check_result_limit(task)
        kwargs = dict(path=path, score=score, summary=summary)
        return getattr(task, result_type)(**kwargs)

    @expose(format='json')
    def to_xml(self, id):
        taskxml = RecipeTask.by_id(id).to_xml().toprettyxml()
        return dict(xml=taskxml)

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def peer_roles(self, task_id):
        try:
            task = RecipeTask.by_id(task_id)
        except NoResultFound:
            raise BX(_('Invalid task ID: %s') % task_id)
        # don't use set, we want to preserve ordering
        roles = {}
        for role, recipes in task.recipe.peer_roles().iteritems():
            fqdns = roles.setdefault(unicode(role), [])
            for recipe in recipes:
                if not recipe.resource or not recipe.resource.fqdn:
                    continue
                fqdn = unicode(recipe.resource.fqdn)
                if fqdn not in fqdns:
                    fqdns.append(fqdn)
        for role, tasks in task.peer_roles().iteritems():
            fqdns = roles.setdefault(unicode(role), [])
            for task in tasks:
                if not task.recipe.resource or not task.recipe.resource.fqdn:
                    continue
                fqdn = unicode(task.recipe.resource.fqdn)
                if fqdn not in fqdns:
                    fqdns.append(fqdn)
        return roles
예제 #6
0
            searchvalue = kw['recipesearch']
            recipes_found = self._recipe_search(recipe, **kw)
            return_dict.update({'recipes_found': recipes_found})
            return_dict.update({'searchvalue': searchvalue})
            return_dict.update({'simplesearch': simplesearch})
        return return_dict

    @expose(template='bkr.server.templates.grid')
    @paginate('list', default_order='-id', limit=50)
    def index(self, *args, **kw):
        return self.recipes(
            recipes=session.query(Recipe).filter_by(type='machine_recipe'),
            *args,
            **kw)

    @identity.require(identity.not_anonymous())
    @expose(template='bkr.server.templates.grid')
    @paginate('list', default_order='-id', limit=50)
    def mine(self, *args, **kw):
        return self.recipes(recipes=MachineRecipe.mine(identity.current.user),
                            action='./mine',
                            *args,
                            **kw)

    def recipes(self, recipes, action='.', *args, **kw):
        recipes = recipes.join(Recipe.recipeset)\
            .join(RecipeSet.job)\
            .filter(not_(Job.is_deleted))
        recipes_return = self._recipes(recipes, **kw)
        searchvalue = None
        search_options = {}
예제 #7
0
class Preferences(RPCRoot):

    exposed = True
    delete_link = DeleteLinkWidgetForm()
    beaker_password = widgets.PasswordField(name='password',
                                            label='Beaker Password')
    root_password = widgets.TextField(name='_root_password',
                                      label='Root Password')
    rootpw_expiry = widgets.TextField(name='rootpw_expiry',
                                      label='Root Password Expiry',
                                      attrs={'disabled': True})
    email = widgets.TextField(name='email_address',
                              label='Email Address',
                              validator=validators.Email())
    prefs_form = HorizontalForm(
        'UserPrefs',
        fields=[email, beaker_password, root_password, rootpw_expiry],
        action='save',
        submit_text=_(u'Change'),
    )

    sshkey = widgets.TextArea(
        name='ssh_pub_key',
        label='Public SSH Key',
        validator=beaker_validators.SSHPubKey(not_empty=True))
    ssh_key_add_form = InlineForm(
        'ssh_key_add',
        fields=[sshkey],
        action='ssh_key_add',
        submit_text=_(u'Add'),
    )

    rootpw_grid = BeakerDataGrid(fields=[
        BeakerDataGrid.Column('root_password',
                              title=_(u'Root Password'),
                              getter=lambda x: x.value),
        BeakerDataGrid.Column('effective_from',
                              title=_(u'Effective from'),
                              getter=lambda x: x.valid_from,
                              options=dict(datetime=True)),
    ])

    auto_users = AutoCompleteField(name='user',
                                   search_controller=url("../users/by_name"),
                                   search_param="input",
                                   result_name="matches")

    submission_delegate_form = InlineForm(
        'SubmissionDelegates',
        fields=[auto_users],
        action='save_data',
        submit_text=_(u'Add'),
    )
    remove_submission_delegate_link = DoAndConfirmForm()

    def show_submission_delegates(self, user):
        user_fields = [
            ('Submission Delegate', lambda x: x.display_name),
            ('Action', lambda x: self.remove_submission_delegate_link. \
                display({'delegate_id': x.user_id},
                action=url('remove_submission_delegate'), look='link',
                msg='Are you sure you want to remove %s as a submitter?' % x,
                action_text='Remove (-)')),]
        return BeakerDataGrid(fields=user_fields)

    @expose(template='bkr.server.templates.prefs')
    @identity.require(identity.not_anonymous())
    def index(self, *args, **kw):
        user = identity.current.user

        # Show all future root passwords, and the previous five
        rootpw = ConfigItem.by_name('root_password')
        rootpw_values = rootpw.values().filter(rootpw.value_class.valid_from > datetime.utcnow())\
                       .order_by(rootpw.value_class.valid_from.desc()).all()\
                      + rootpw.values().filter(rootpw.value_class.valid_from <= datetime.utcnow())\
                       .order_by(rootpw.value_class.valid_from.desc())[:5]

        return dict(
            title='User Prefs',
            delete_link=self.delete_link,
            prefs_form=self.prefs_form,
            ssh_key_form=self.ssh_key_add_form,
            widgets={},
            ssh_keys=user.sshpubkeys,
            value=user,
            rootpw=rootpw.current_value(),
            rootpw_grid=self.rootpw_grid,
            rootpw_values=rootpw_values,
            options=None,
            #Hack, to insert static content for submission_delegate
            remove_submission_delegate=self.remove_submission_delegate_link,
            submission_delegates_grid=self.show_submission_delegates(user),
            submission_delegate_form=self.submission_delegate_form)

    # XMLRPC interface
    @expose()
    @identity.require(identity.not_anonymous())
    def remove_submission_delegate_by_name(self,
                                           delegate_name,
                                           service=u'XMLRPC'):
        user = identity.current.user
        try:
            submission_delegate = User.by_user_name(delegate_name)
        except NoResultFound:
            raise BX(_(u'%s is not a valid user name' % delegate_name))
        try:
            user.remove_submission_delegate(submission_delegate,
                                            service=service)
        except ValueError:
            raise BX(_(u'%s is not a submission delegate of %s' % \
                (delegate_name, user)))
        return delegate_name

    # UI interface
    @expose()
    @identity.require(identity.not_anonymous())
    def remove_submission_delegate(self, delegate_id, service=u'WEBUI'):
        user = identity.current.user
        try:
            submission_delegate = User.by_id(delegate_id)
        except NoResultFound:
            flash(_(u'%s is not a valid user id' % delegate_id))
            redirect('.')
        user.remove_submission_delegate(submission_delegate, service=service)
        flash(_(u'%s removed as a submission delegate' % submission_delegate))
        redirect('.')

    # XMLRPC Interface
    @expose()
    @identity.require(identity.not_anonymous())
    def add_submission_delegate_by_name(self,
                                        new_delegate_name,
                                        service=u'XMLRPC'):
        user = identity.current.user
        new_delegate = User.by_user_name(new_delegate_name)
        if not new_delegate:
            raise BX(_(u'%s is not a valid user' % new_delegate_name))
        user.add_submission_delegate(new_delegate, service)
        return new_delegate_name

    # UI Interface
    @expose()
    @identity.require(identity.not_anonymous())
    def add_submission_delegate(self, **kwargs):
        user = identity.current.user
        new_delegate_name = kwargs['user']['text']
        new_delegate = User.by_user_name(new_delegate_name)
        if not new_delegate:
            flash(_(u'%s is not a valid user' % new_delegate_name))
            redirect('.')

        try:
            user.add_submission_delegate(new_delegate, u'WEBUI')
        except NoChangeException, e:
            flash(_(unicode(e)))
            redirect('.')

        flash(_(u'Added %s as a submission delegate' % new_delegate_name))
        redirect('.')
예제 #8
0
    def filter(self, filters):
        """
        Returns a list of details for jobs filtered by the given criteria.

        The *filter* argument must be a an XML-RPC structure (dict) specifying
        filter criteria. The following keys are recognised:

            'tags'
                List of job tags.
            'daysComplete'
                Number of days elapsed since the jobs completion.
            'family'
                Job distro family, for example ``'RedHatEnterpriseLinuxServer5'``.
            'product'
                Job product name
            'owner'
                Job owner username
            'mine'
                Inclusion is equivalent to including own username in 'owner'
            'whiteboard'
                Job whiteboard (substring match)
            'limit'
                Integer limit to number of jobs returned.
            'minid'
                Min JobID of the jobs to search
            'maxid'
                Maximum Job ID of the jobs to search

        Returns a two-element array. The first element is an array of JobIDs
        of the form ``'J:123'``, suitable to be passed to the
        :meth:`jobs.delete_jobs` method. The second element is a human-readable
        count of the number of Jobs matched. Does not return deleted jobs.
        """

        # if  min/max/both IDs have been specified, filter it right here
        minid = filters.get('minid', None)
        maxid = filters.get('maxid', None)
        jobs = session.query(Job)
        if minid:
            jobs = jobs.filter(Job.id >= minid)
        if maxid:
            jobs = jobs.filter(Job.id <= maxid)

        tags = filters.get('tags', None)
        complete_days = filters.get('daysComplete', None)
        family = filters.get('family', None)
        product = filters.get('product', None)
        owner = filters.get('owner', None)
        whiteboard = filters.get('whiteboard', None)
        mine = filters.get('mine', None)
        limit = filters.get('limit', None)

        if mine and not identity.not_anonymous():
            raise BX(
                _('You should be authenticated to use the --mine filter.'))

        if mine and identity.not_anonymous():
            if owner:
                if type(owner) is list:
                    owner.append(identity.current.user.user_name)
                else:
                    owner = [owner, identity.current.user.user_name]
            else:
                owner = identity.current.user.user_name

        jobs = jobs.order_by(Job.id.desc())
        if tags:
            jobs = Job.by_tag(tags, jobs)
        if complete_days:
            jobs = Job.complete_delta({'days': int(complete_days)}, jobs)
        if family:
            jobs = Job.has_family(family, jobs)
        if product:
            jobs = Job.by_product(product, jobs)
        if owner:
            jobs = Job.by_owner(owner, jobs)
        if whiteboard:
            jobs = jobs.filter(Job.whiteboard.like(u'%%%s%%' % whiteboard))

        jobs = Job.sanitise_jobs(jobs)

        if limit:
            limit = int(limit)
            jobs = jobs.limit(limit)

        jobs = jobs.values(Job.id)

        return_value = ['J:%s' % j[0] for j in jobs]
        return return_value
예제 #9
0
class CSV(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = False

    export_help_text = XML(
        u'<span>Refer to the <a href="http://beaker-project.org/docs/'
        'admin-guide/interface.html#export" target="_blank">'
        'documentation</a> to learn more about the exported data.</span>'
    ).expand()
    import_help_text = XML(
        u'<span>Refer to the <a href="http://beaker-project.org/docs/'
        'admin-guide/interface.html#import" target="_blank">'
        'documentation</a> for details about the supported CSV format.</span>'
    ).expand()

    upload     = widgets.FileField(name='csv_file', label='Import CSV', \
                                   help_text = import_help_text)
    download = RadioButtonList(name='csv_type',
                               label='CSV Type',
                               options=[
                                   ('system', 'Systems'),
                                   ('system_id', 'Systems (for modification)'),
                                   ('labinfo', 'System LabInfo'),
                                   ('power', 'System Power'),
                                   ('exclude', 'System Excluded Families'),
                                   ('install', 'System Install Options'),
                                   ('keyvalue', 'System Key/Values'),
                                   ('system_pool', 'System Pools'),
                                   ('user_group', 'User Groups')
                               ],
                               default='system',
                               help_text=export_help_text)

    importform = HorizontalForm(
        'import',
        fields=[upload],
        action='import data',
        submit_text=_(u'Import CSV'),
    )

    exportform = HorizontalForm(
        'export',
        fields=[download],
        action='export data',
        submit_text=_(u'Export CSV'),
    )

    @expose(template='bkr.server.templates.form')
    @identity.require(identity.not_anonymous())
    def index(self, **kw):
        return dict(
            form=self.exportform,
            title=_(u'CSV Export'),
            action='./action_export',
            options={},
            value=kw,
        )

    @expose(template='bkr.server.templates.form-post')
    @identity.require(identity.in_group('admin'))
    def csv_import(self, **kw):
        return dict(
            form=self.importform,
            title=_(u'CSV Import'),
            action='./action_import',
            options={},
            value=kw,
        )

    @expose()
    @identity.require(identity.not_anonymous())
    def action_export(self, csv_type, *args, **kw):
        file = NamedTemporaryFile()
        log = self.to_csv(file, csv_type)
        file.seek(0)

        return serve_file(file.name,
                          contentType="text/csv",
                          disposition="attachment",
                          name="%s.csv" % csv_type)

    def _import_row(self, data, log):
        if data['csv_type'] in system_types and ('fqdn' in data
                                                 or 'id' in data):
            if data.get('id', None):
                try:
                    system = System.query.filter(System.id == data['id']).one()
                except InvalidRequestError as e:
                    raise ValueError('Non-existent system id')
            else:
                try:
                    system = System.query.filter(
                        System.fqdn == data['fqdn']).one()
                except InvalidRequestError:
                    # Create new system with some defaults
                    # Assume the system is broken until proven otherwise.
                    # Also assumes its a machine.  we have to pick something
                    system = System(fqdn=data['fqdn'],
                                    owner=identity.current.user,
                                    type=SystemType.machine,
                                    status=SystemStatus.broken)
                    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)
            if not system.can_edit(identity.current.user):
                raise ValueError('You are not the owner of %s' % system.fqdn)
            # we change the FQDN only when a valid system id is supplied
            if not data.get('id', None):
                data.pop('fqdn')
            self.from_csv(system, data, log)
        elif data['csv_type'] == 'user_group' and 'user' in data:
            user = User.by_user_name(data['user'])
            if user is None:
                raise ValueError('%s is not a valid user' % data['user'])
            CSV_GroupUser.from_csv(user, data, log)
        else:
            raise ValueError('Invalid csv_type %s or missing required fields' %
                             data['csv_type'])

    @expose(template='bkr.server.templates.csv_import')
    @identity.require(identity.in_group('admin'))
    def action_import(self, csv_file, *args, **kw):
        """
        TurboGears method to import data from csv
        """

        log = []
        try:
            # ... process CSV file contents here ...
            missing = object()
            reader = UnicodeDictReader(csv_file.file,
                                       restkey=missing,
                                       restval=missing)
            is_empty = True

            for data in reader:

                is_empty = False

                if missing in data:
                    log.append('Too many fields on line %s (expecting %s)' %
                               (reader.line_num, len(reader.fieldnames)))
                    continue
                if any(value is missing for value in data.itervalues()):
                    missing_fields = [
                        field for field, value in data.iteritems()
                        if value is missing
                    ]
                    log.append('Missing fields on line %s: %s' %
                               (reader.line_num, ', '.join(missing_fields)))
                    continue
                if 'csv_type' not in data:
                    log.append('Missing csv_type on line %s' % reader.line_num)
                    continue
                try:
                    with session.begin_nested():
                        self._import_row(data, log)
                except Exception, e:
                    # log and continue processing more rows
                    log.append('Error importing line %s: %s' %
                               (reader.line_num, e))

            if is_empty:
                log.append('Empty CSV file supplied')

        except csv.Error, e:
            session.rollback()
            log.append('Error parsing CSV file: %s' % e)
예제 #10
0
파일: distro.py 프로젝트: xhernandez/beaker
class Distros(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True

    tag_form = DistroTags(name='tags')

    @expose(template="bkr.server.templates.distro")
    def view(self, id=None, *args, **kw):
        try:
            distro = Distro.by_id(id)
        except InvalidRequestError:
            flash(_(u"Invalid distro id %s" % id))
            redirect(".")
        is_admin = identity.current.user and identity.current.user.is_admin(
        ) or False
        task_form = TaskSearchForm(hidden=dict(distro=True, osmajor_id=True))
        return dict(title='Distro',
                    value=distro,
                    value_task=dict(distro_id=distro.id),
                    form=self.tag_form,
                    form_task=task_form,
                    action='./save_tag',
                    action_task='/tasks/do_search',
                    options=dict(tags=distro.tags, readonly=not is_admin))

    @expose()
    def get_osmajors(self, tags=None):
        """
        Returns a list of all distro families. If *tags* is given, limits to
        distros with at least one of the given tags.
        """
        osmajors = session.query(OSMajor.osmajor)
        if tags:
            osmajors = osmajors\
                .join(OSMajor.osversions, OSVersion.distros, Distro.trees)\
                .filter(DistroTree.lab_controller_assocs.any())\
                .filter(Distro._tags.any(DistroTag.tag.in_(tags)))
        return [osmajor for osmajor, in osmajors.distinct()]

    @expose()
    def get_osmajor(self, distro):
        """ pass in a distro name and get back the osmajor is belongs to.
        """
        try:
            osmajor = '%s' % Distro.by_name(distro).osversion.osmajor
        except DatabaseLookupError:
            raise BX(_('Invalid Distro: %s' % distro))
        return osmajor

    get_family = get_osmajor

    @expose()
    def get_arch(self, filter):
        """
        Pass in a dict() with either `distro` or `osmajor` to get possible arches.
        Further supported filters are `variant` and `tags`.
        """
        distros = Distro.query
        if 'distro' in filter:
            distros = distros.filter(Distro.name == filter['distro'])
        if 'osmajor' in filter:
            distros = distros.join(Distro.osversion).join(OSVersion.osmajor)\
                .filter(OSMajor.osmajor == filter['osmajor'])
        if filter.get('variant'):
            distros = distros.join(Distro.trees)\
                .filter(DistroTree.variant == filter['variant'])
        for tag in filter.get('tags', []):
            distros = distros.filter(Distro._tags.any(DistroTag.tag == tag))
        # approximates the behaviour of <distroRequires/>
        distro = distros.order_by(Distro.date_created.desc()).first()
        if distro is None:
            raise BX(_('No distros match given filter: %r') % filter)
        return [arch.arch for arch in distro.osversion.arches]

    @expose()
    @identity.require(identity.has_permission('tag_distro'))
    def save_tag(self, id=None, tag=None, *args, **kw):
        try:
            distro = Distro.by_id(id)
        except InvalidRequestError:
            flash(_(u"Invalid distro id %s" % id))
            redirect(".")
        if tag['text']:
            distro.tags.append(tag['text'])
            distro.activity.append(
                DistroActivity(user=identity.current.user,
                               service=u'WEBUI',
                               action=u'Added',
                               field_name=u'Tag',
                               old_value=None,
                               new_value=tag['text']))
        flash(_(u"Added Tag %s" % tag['text']))
        redirect("./view?id=%s" % id)

    @cherrypy.expose
    @identity.require(identity.has_permission('distro_expire'))
    def expire(self, name, service=u'XMLRPC'):
        distro = Distro.by_name(name)
        distro.expire(service)

    @expose()
    @identity.require(identity.has_permission('tag_distro'))
    @restrict_http_method('post')
    def tag_remove(self, id=None, tag=None, *args, **kw):
        try:
            distro = Distro.by_id(id)
        except InvalidRequestError:
            flash(_(u"Invalid distro id %s" % id))
            redirect(".")
        if tag:
            for dtag in distro.tags:
                if dtag == tag:
                    distro.tags.remove(dtag)
                    distro.activity.append(
                        DistroActivity(user=identity.current.user,
                                       service=u'WEBUI',
                                       action=u'Removed',
                                       field_name=u'Tag',
                                       old_value=tag,
                                       new_value=None))
                    flash(_(u"Removed Tag %s" % tag))
        redirect("./view?id=%s" % id)

    def _distros(self, distro, **kw):
        return_dict = {}
        if 'simplesearch' in kw:
            simplesearch = kw['simplesearch']
            kw['distrosearch'] = [{
                'table': 'Name',
                'operation': 'contains',
                'value': kw['simplesearch']
            }]
        else:
            simplesearch = None

        return_dict.update({'simplesearch': simplesearch})
        if kw.get("distrosearch"):
            searchvalue = kw['distrosearch']
            distros_found = self._distro_search(distro, **kw)
            return_dict.update({'distros_found': distros_found})
            return_dict.update({'searchvalue': searchvalue})
        return return_dict

    def _distro_search(self, distro, **kw):
        distro_search = search_utility.Distro.search(distro)
        for search in kw['distrosearch']:
            col = search['table']
            distro_search.append_results(search['value'], col,
                                         search['operation'], **kw)
        return distro_search.return_results()

    @expose(template="bkr.server.templates.grid")
    @paginate('list', default_order='-date_created', limit=50)
    def index(self, *args, **kw):
        distro_q = session.query(Distro).outerjoin(Distro.osversion, OSVersion.osmajor)\
                .filter(Distro.trees.any(DistroTree.lab_controller_assocs.any()))
        return self.distros(distros=distro_q, *args, **kw)

    @expose(template="bkr.server.templates.grid")
    @paginate('list', default_order='-date_created', limit=50)
    def name(self, *args, **kw):
        distro_q = session.query(Distro).join(Distro.osversion, OSVersion.osmajor)\
                .filter(Distro.trees.any(DistroTree.lab_controller_assocs.any()))\
                .filter(Distro.name.like(kw['name']))
        return self.distros(distros=distro_q, action='./name')

    def distros(self, distros, action='.', *args, **kw):
        distros_return = self._distros(distros, **kw)
        searchvalue = None
        hidden_fields = None
        search_options = {}
        if distros_return:
            if 'distros_found' in distros_return:
                distros = distros_return['distros_found']
            if 'searchvalue' in distros_return:
                searchvalue = distros_return['searchvalue']
            if 'simplesearch' in distros_return:
                search_options['simplesearch'] = distros_return['simplesearch']

        distros_grid = myPaginateDataGrid(fields=[
            myPaginateDataGrid.Column(
                name='id',
                getter=lambda x: make_link(url='/distros/view?id=%s' % x.id,
                                           text=x.id),
                title='ID',
                options=dict(sortable=True)),
            myPaginateDataGrid.Column(
                name='name',
                getter=lambda x: make_link(url='/distros/view?id=%s' % x.id,
                                           text=x.name),
                title='Name',
                options=dict(sortable=True)),
            myPaginateDataGrid.Column(name='osversion.osmajor.osmajor',
                                      getter=lambda x: x.osversion.osmajor,
                                      title='OS Major Version',
                                      options=dict(sortable=True)),
            myPaginateDataGrid.Column(name='osversion.osminor',
                                      getter=lambda x: x.osversion.osminor,
                                      title='OS Minor Version',
                                      options=dict(sortable=True)),
            myPaginateDataGrid.Column(name='date_created',
                                      getter=lambda x: x.date_created,
                                      title='Date Created',
                                      options=dict(sortable=True,
                                                   datetime=True)),
        ])

        if 'tag' in kw:
            hidden_fields = [('tag', kw['tag'])]

        search_bar = SearchBar(
            name='distrosearch',
            label=_(u'Distro Search'),
            table=search_utility.Distro.search.create_complete_search_table(),
            search_controller=url("/get_search_options_distros"),
            extra_hiddens=hidden_fields,
            date_picker=['created'])

        return dict(title="Distros",
                    grid=distros_grid,
                    search_bar=search_bar,
                    action=action,
                    options=search_options,
                    searchvalue=searchvalue,
                    list=distros)

    #XMLRPC method for listing distros
    @cherrypy.expose
    def filter(self, filter):
        """
        .. seealso:: :meth:`distrotrees.filter`

        Returns a list of details for distros filtered by the given criteria.

        The *filter* argument must be an XML-RPC structure (dict) specifying 
        filter criteria. The following keys are recognised:

            'name'
                Distro name. May include % SQL wildcards, for example 
                ``'%20101121.nightly'``.
            'family'
                Distro family name, for example ``'RedHatEnterpriseLinuxServer5'``. 
                Matches are exact.
            'distroid'
                Distro id.
                Matches are exact.
            'tags'
                List of distro tags, for example ``['STABLE', 'RELEASED']``. All given 
                tags must be present on the distro for it to match.
            'limit'
                Integer limit to number of distros returned.

        The return value is an array with one element per distro (up to the 
        maximum number of distros given by 'limit'). Each element is an XML-RPC 
        structure (dict) describing a distro.

        .. versionchanged:: 0.9
           Some return columns were removed, because they no longer apply to 
           distros in Beaker. Use the new :meth:`distrotrees.filter` method 
           to fetch details of distro trees.
        """
        distros = session.query(Distro)
        name = filter.get('name', None)
        family = filter.get('family', None)
        distroid = filter.get('distroid', None)
        tags = filter.get('tags', None) or []
        limit = filter.get('limit', None)
        for tag in tags:
            distros = distros.filter(Distro._tags.any(DistroTag.tag == tag))
        if name:
            distros = distros.filter(Distro.name.like('%s' % name))
        if distroid:
            distros = distros.filter(Distro.id == int(distroid))
        if family:
            distros = distros.join(Distro.osversion, OSVersion.osmajor)
            distros = distros.filter(OSMajor.osmajor == '%s' % family)
        # we only want distros that are active in at least one lab controller
        distros = distros.filter(
            Distro.trees.any(DistroTree.lab_controller_assocs.any()))
        distros = distros.order_by(Distro.date_created.desc())
        if limit:
            distros = distros[:limit]
        return [{
            'distro_id': distro.id,
            'distro_name': distro.name,
            'distro_version': unicode(distro.osversion),
            'distro_tags': [unicode(tag) for tag in distro.tags],
        } for distro in distros]

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def edit_version(self, name, version):
        """
        Updates the version for all distros with the given name.

        :param name: name of distros to be updated, for example 
            'RHEL5.6-Server-20101110.0'
        :type name: string
        :param version: new version to be applied, for example 
            'RedHatEnterpriseLinuxServer5.6' or 'Fedora14'
        :type version: string
        """
        distros = Distro.query.filter(Distro.name.like(unicode(name)))
        edited = []

        os_major = version.split('.')[0]

        # Try and split OSMinor
        try:
            os_minor = version.split('.')[1]
        except IndexError:
            os_minor = '0'

        # Try and find OSMajor
        osmajor = OSMajor.lazy_create(osmajor=os_major)

        # Try and find OSVersion
        osversion = OSVersion.lazy_create(osmajor=osmajor, osminor=os_minor)

        # Check each Distro
        for distro in distros:
            if osversion != distro.osversion:
                edited.append('%s' % distro.name)
                distro.activity.append(
                    DistroActivity(user=identity.current.user,
                                   service=u'XMLRPC',
                                   field_name=u'osversion',
                                   action=u'Changed',
                                   old_value=unicode(distro.osversion),
                                   new_value=unicode(osversion)))
                distro.osversion = osversion
        return edited

    @cherrypy.expose
    @identity.require(identity.has_permission('tag_distro'))
    def tag(self, name, tag):
        """
        Applies the given tag to all matching distros.

        :param name: distro name to filter by (may include SQL wildcards)
        :type name: string or nil
        :param tag: tag to be applied
        :type tag: string
        :returns: list of distro names which have been modified

        .. versionchanged:: 0.9
           Removed *arch* parameter. Tags apply to distros and not distro trees.
        """
        added = []
        distros = Distro.query.filter(Distro.name.like('%s' % name))
        for distro in distros:
            if tag not in distro.tags:
                added.append('%s' % distro.name)
                distro.activity.append(
                    DistroActivity(user=identity.current.user,
                                   service=u'XMLRPC',
                                   action=u'Added',
                                   field_name=u'Tag',
                                   old_value=None,
                                   new_value=tag))
                distro.tags.append(tag)
        return added

    @cherrypy.expose
    @identity.require(identity.has_permission('tag_distro'))
    def untag(self, name, tag):
        """
        Like :meth:`distros.tag` but the opposite.
        """
        removed = []
        distros = Distro.query.filter(Distro.name.like('%s' % name))
        for distro in distros:
            if tag in distro.tags:
                removed.append('%s' % distro.name)
                distro.activity.append(
                    DistroActivity(user=identity.current.user,
                                   service=u'XMLRPC',
                                   action=u'Removed',
                                   field_name=u'Tag',
                                   old_value=tag,
                                   new_value=None))
                distro.tags.remove(tag)
        return removed

    default = index
예제 #11
0
class ReserveWorkflow:
    @identity.require(identity.not_anonymous())
    @expose(template='bkr.server.templates.reserve_workflow')
    def index(self, **kwargs):
        # CherryPy will give us distro_tree_id as a scalar if it only has one
        # value, but we want it to always be a list of int
        if not kwargs.get('distro_tree_id'):
            kwargs['distro_tree_id'] = []
        elif not isinstance(kwargs['distro_tree_id'], list):
            kwargs['distro_tree_id'] = [int(kwargs['distro_tree_id'])]
        else:
            kwargs['distro_tree_id'] = [
                int(x) for x in kwargs['distro_tree_id']
            ]

        # If we got a distro_tree_id but no osmajor or distro, fill those in
        # with the right values so that the distro picker is populated properly
        if kwargs['distro_tree_id']:
            distro_tree = DistroTree.by_id(kwargs['distro_tree_id'][0])
            if not kwargs.get('distro'):
                kwargs['distro'] = distro_tree.distro.name
            if not kwargs.get('osmajor'):
                kwargs[
                    'osmajor'] = distro_tree.distro.osversion.osmajor.osmajor

        options = {}
        options['tag'] = [tag.tag for tag in DistroTag.used()]
        options['osmajor'] = [
            osmajor.osmajor
            for osmajor in OSMajor.ordered_by_osmajor(OSMajor.in_any_lab())
        ]
        options['distro'] = self._get_distro_options(
            osmajor=kwargs.get('osmajor'), tag=kwargs.get('tag'))
        options['distro_tree_id'] = self._get_distro_tree_options(
            distro=kwargs.get('distro'))
        options['lab'] = [
            lc.fqdn
            for lc in LabController.query.filter(LabController.removed == None)
        ]
        return dict(title=_(u'Reserve Workflow'),
                    selection=kwargs,
                    options=options)

    @expose(allow_json=True)
    def get_distro_options(self, **kwargs):
        return {'options': self._get_distro_options(**kwargs)}

    def _get_distro_options(self,
                            osmajor=None,
                            tag=None,
                            system=None,
                            **kwargs):
        """
        Returns a list of distro names for the given osmajor and tag.
        """
        if not osmajor:
            return []
        distros = Distro.query.join(Distro.osversion, OSVersion.osmajor)\
                .filter(Distro.trees.any(DistroTree.lab_controller_assocs.any()))\
                .filter(OSMajor.osmajor == osmajor)\
                .order_by(Distro.date_created.desc())
        if tag:
            distros = distros.filter(Distro._tags.any(DistroTag.tag == tag))
        if system:
            try:
                system = System.by_fqdn(system, identity.current.user)
            except DatabaseLookupError:
                return []
            distros = system.distros(query=distros)
        return [name for name, in distros.values(Distro.name)]

    @expose(allow_json=True)
    def get_distro_tree_options(self, **kwargs):
        return {'options': self._get_distro_tree_options(**kwargs)}

    def _get_distro_tree_options(self, distro=None, system=None, **kwargs):
        """
        Returns a list of distro trees for the given distro.
        """
        if not distro:
            return []
        try:
            distro = Distro.by_name(distro)
        except DatabaseLookupError:
            return []
        trees = distro.dyn_trees.join(DistroTree.arch)\
                .filter(DistroTree.lab_controller_assocs.any())\
                .order_by(DistroTree.variant, Arch.arch)
        if system:
            try:
                system = System.by_fqdn(system, identity.current.user)
            except DatabaseLookupError:
                return []
            trees = system.distro_trees(query=trees)
        return [(tree.id, unicode(tree)) for tree in trees]
예제 #12
0
class TaskActions(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True
    unstoppable_task_types = [Recipe, RecipeTaskResult]

    task_types = dict(J=Job,
                      RS=RecipeSet,
                      R=Recipe,
                      T=RecipeTask,
                      TR=RecipeTaskResult)

    stoppable_task_types = dict([(rep, obj)
                                 for rep, obj in task_types.iteritems()
                                 if obj not in unstoppable_task_types])

    @cherrypy.expose
    def task_info(self, taskid, flat=True):
        """
        Returns an XML-RPC structure (dict) describing the current state of the 
        given job component.

        :param taskid: see above
        :type taskid: string
        """
        return TaskBase.get_by_t_id(taskid).task_info()

    @cherrypy.expose
    def to_xml(self, taskid, clone=False, from_job=True):
        """
        Returns an XML representation of the given job component, including its 
        current state.

        :param taskid: see above
        :type taskid: string
        """
        task_type, task_id = taskid.split(":")
        if task_type.upper() in self.task_types.keys():
            try:
                task = self.task_types[task_type.upper()].by_id(task_id)
            except InvalidRequestError:
                raise BX(_("Invalid %s %s" % (task_type, task_id)))
        return task.to_xml(clone, from_job).toxml()

    @cherrypy.expose
    def files(self, taskid):
        """
        Returns an array of XML-RPC structures (dicts) describing each of the 
        result files for the given job component and its descendants.
        """
        return TaskBase.get_by_t_id(taskid).all_logs

    @identity.require(identity.not_anonymous())
    @cherrypy.expose
    def stop(self, taskid, stop_type, msg):
        """
        Cancels the given job. Note that when cancelling some part of a job 
        (for example, by passing *taskid* starting with ``R:`` to indicate 
        a particular recipe within a job) the entire job is cancelled.

        :param taskid: see above
        :type taskid: string
        :param stop_type: must be ``'cancel'`` (other values are reserved for 
            Beaker's internal use)
        :type stop_type: string
        :param msg: reason for cancelling
        :type msg: string
        """
        task_type, task_id = taskid.split(":")
        if task_type.upper() in self.stoppable_task_types.keys():
            try:
                task = self.stoppable_task_types[task_type.upper()].by_id(
                    task_id)
            except InvalidRequestError:
                raise BX(_("Invalid %s %s" % (task_type, task_id)))
        else:
            raise BX(_("Task type %s is not stoppable" % (task_type)))
        if stop_type not in task.stop_types:
            raise BX(
                _('Invalid stop_type: %s, must be one of %s' %
                  (stop_type, task.stop_types)))
        if not task.can_stop(identity.current.user):
            raise BX(
                _("You don't have permission to %s %s" % (stop_type, taskid)))
        kwargs = dict(msg=msg)
        task.record_activity(user=identity.current.user,
                             service=u'XMLRPC',
                             field=u'Status',
                             action=u'Cancelled',
                             old='',
                             new='')
        try:
            return getattr(task, stop_type)(**kwargs)
        except StaleTaskStatusException:
            raise BX(
                _(u"Could not cancel job id %s. Please try later" % task_id))
예제 #13
0
class SystemsController(controllers.Controller):
    # For XMLRPC methods in this class.
    exposed = True

    @expose()
    @identity.require(identity.not_anonymous())
    def reserve(self, fqdn):
        """
        "Reserves" (a.k.a. "takes") the system with the given fully-qualified domain 
        name. The caller then becomes the user of the system, and can 
        provision it at will.

        A system may only be reserved when: its condition is 'Manual', it is not 
        currently in use, and the caller has permission to use the system.

        .. versionadded:: 0.6
        """
        system = System.by_fqdn(fqdn, identity.current.user)
        system.reserve_manually(service=u'XMLRPC')
        return system.fqdn  # because turbogears makes us return something

    @expose()
    @identity.require(identity.not_anonymous())
    def release(self, fqdn):
        """
        Releases a reservation on the system with the given fully-qualified 
        domain name.

        The caller must be the current user of a system (i.e. must have 
        successfully reserved it previously).

        .. versionadded:: 0.6
        """
        system = System.by_fqdn(fqdn, identity.current.user)
        system.unreserve_manually_reserved(service=u'XMLRPC')
        return system.fqdn  # because turbogears makes us return something

    @expose()
    @identity.require(identity.not_anonymous())
    def delete(self, fqdn):
        """
        Delete a system with the given fully-qualified domain name.

        The caller must be the owner of the system or an admin.

        :param fqdn: fully-qualified domain name of the system to be deleted
        :type fqdn: string

        .. versionadded:: 0.8.2
        """
        system = System.by_fqdn(fqdn, identity.current.user)
        if system.reservations:
            raise ValueError("Can't delete system %s with reservations" % fqdn)
        if system.owner != identity.current.user and \
           not identity.current.user.is_admin():
            raise ValueError("Can't delete system %s you don't own" % fqdn)
        session.delete(system)
        return 'Deleted %s' % fqdn

    @expose()
    @identity.require(identity.not_anonymous())
    def power(self, action, fqdn, clear_netboot=False, force=False, delay=0):
        """
        Controls power for the system with the given fully-qualified domain 
        name.

        If the *clear_netboot* argument is True, the Cobbler netboot 
        configuration for the system will be cleared before power controlling.

        Controlling power for a system is not normally permitted when the 
        system is in use by someone else, because it is likely to interfere 
        with their usage. Callers may pass True for the *force* argument to 
        override this safety check.

        This method does not wait for Cobbler to report whether the power 
        control was succesful.

        :param action: 'on', 'off', or 'reboot'
        :type action: string
        :param fqdn: fully-qualified domain name of the system to be power controlled
        :type fqdn: string
        :param clear_netboot: whether to clear netboot configuration before powering
        :type clear_netboot: boolean
        :param force: whether to power the system even if it is in use
        :type force: boolean
        :param delay: number of seconds to delay before performing the action (default none)
        :type delay: int or float

        .. versionadded:: 0.6
        .. versionchanged:: 0.6.14
           No longer waits for completion of Cobbler power task.
        """
        system = System.by_fqdn(fqdn, identity.current.user)
        if not system.can_power(identity.current.user):
            raise InsufficientSystemPermissions(
                _(u'User %s does not have permission to power system %s') %
                (identity.current.user, system))
        if not force and system.user is not None \
                and system.user != identity.current.user:
            raise BX(_(u'System is in use'))
        if clear_netboot:
            system.clear_netboot(service=u'XMLRPC')
        system.action_power(action, service=u'XMLRPC', delay=delay)
        return system.fqdn  # because turbogears makes us return something

    @expose()
    @identity.require(identity.not_anonymous())
    def clear_netboot(self, fqdn):
        """
        Clears any netboot configuration in effect for the system with the
        given fully-qualified domain name.

        .. verisonadded:: 0.9
        """
        system = System.by_fqdn(fqdn, identity.current.user)
        system.clear_netboot(service=u'XMLRPC')
        return system.fqdn  # because turbogears makes us return something

    @expose()
    @identity.require(identity.not_anonymous())
    def provision(self,
                  fqdn,
                  distro_tree_id,
                  ks_meta=None,
                  kernel_options=None,
                  kernel_options_post=None,
                  kickstart=None,
                  reboot=True):
        """
        Provisions a system with the given distro tree and options.

        The *ks_meta*, *kernel_options*, and *kernel_options_post* arguments 
        override the default values configured for the system. For example, if 
        the default kernel options for the system/distro are
        'console=ttyS0 ksdevice=eth0', and the caller passes 'ksdevice=eth1' 
        for *kernel_options*, the kernel options used will be
        'console=ttyS0 ksdevice=eth1'.

        :param distro_tree_id: numeric id of distro tree to be provisioned
        :type distro_tree_id: int
        :param ks_meta: kickstart options
        :type ks_meta: str
        :param kernel_options: kernel options for installation
        :type kernel_options: str
        :param kernel_options_post: kernel options for after installation
        :type kernel_options_post: str
        :param kickstart: complete kickstart
        :type kickstart: str
        :param reboot: whether to reboot the system after applying Cobbler changes
        :type reboot: bool

        .. versionadded:: 0.6

        .. versionchanged:: 0.6.10
           System-specific kickstart/kernel options are now obeyed.

        .. versionchanged:: 0.9
           *distro_install_name* parameter is replaced with *distro_tree_id*. 
           See :meth:`distrotrees.filter`.
        """
        system = System.by_fqdn(fqdn, identity.current.user)
        if not system.user == identity.current.user:
            raise BX(_(u'Reserve a system before provisioning'))
        distro_tree = DistroTree.by_id(distro_tree_id)

        # sanity check: does the distro tree apply to this system?
        if not system.compatible_with_distro_tree(distro_tree):
            raise BX(
                _(u'Distro tree %s cannot be provisioned on %s') %
                (distro_tree, system.fqdn))
        if not system.lab_controller:
            raise BX(_(u'System is not attached to a lab controller'))
        if not distro_tree.url_in_lab(system.lab_controller):
            raise BX(
                _(u'Distro tree %s is not available in lab %s') %
                (distro_tree, system.lab_controller))

        if identity.current.user.rootpw_expired:
            raise BX(
                _('Your root password has expired, please change or clear it in order to submit jobs.'
                  ))

        # ensure system-specific defaults are used
        # (overriden by this method's arguments)
        options = system.manual_provision_install_options(distro_tree)\
            .combined_with(InstallOptions.from_strings(
                    ks_meta or '',
                    kernel_options or '',
                    kernel_options_post or ''))
        if 'ks' not in options.kernel_options:
            rendered_kickstart = generate_kickstart(options,
                                                    distro_tree=distro_tree,
                                                    system=system,
                                                    user=identity.current.user,
                                                    kickstart=kickstart)
            options.kernel_options['ks'] = rendered_kickstart.link
        system.configure_netboot(distro_tree,
                                 options.kernel_options_str,
                                 service=u'XMLRPC')
        system.record_activity(user=identity.current.user,
                               service=u'XMLRPC',
                               action=u'Provision',
                               field=u'Distro Tree',
                               old=u'',
                               new=u'Success: %s' % distro_tree)

        if reboot:
            system.action_power(action='reboot', service=u'XMLRPC')

        return system.fqdn  # because turbogears makes us return something

    @expose()
    def history(self, fqdn, since=None):
        """
        Returns the history for the given system.
        If the *since* argument is given, all history entries between that 
        timestamp and the present are returned. By default, history entries 
        from the past 24 hours are returned.

        History entries are returned as a list of structures (dicts), each of 
        which has the following keys:

            'created'
                Timestamp of the activity
            'user'
                Username of the user who performed the action
            'service'
                Service by which the action was performed (e.g. 'XMLRPC')
            'action'
                Action which was performed (e.g. 'Changed')
            'field_name'
                Name of the field which was acted upon
            'old_value'
                Value of the field before the action (if any)
            'new_value'
                Value of the field after the action (if any)

        Note that field names and actions are recorded in human-readable form, 
        which might not be ideal for machine parsing.

        All timestamps are expressed in UTC.

        .. versionadded:: 0.6.6
        """
        if since is None:
            since = datetime.datetime.utcnow() - datetime.timedelta(days=1)
        else:
            if not isinstance(since, datetime.datetime):
                raise TypeError("'since' must be an XML-RPC datetime")
        system = System.by_fqdn(fqdn, identity.current.user)
        activities = SystemActivity.query.filter(
            and_(SystemActivity.object == system,
                 SystemActivity.created >= since))
        return [
            dict(created=a.created,
                 user=a.user.user_name,
                 service=a.service,
                 action=a.action,
                 field_name=a.field_name,
                 old_value=a.old_value,
                 new_value=a.new_value) for a in activities
        ]

    @cherrypy.expose()
    @identity.require(identity.not_anonymous())
    def get_osmajor_arches(self, fqdn, tags=None):
        """
        Returns a dict of all distro families with a list of arches that apply for system.
        If *tags* is given, limits to distros with at least one of the given tags.

        {"RedHatEnterpriseLinux3": ["i386", "x86_64"],}

        .. versionadded:: 0.11.0
        """
        system = System.by_fqdn(fqdn, identity.current.user)
        query = system.distro_trees(only_in_lab=False)
        if tags:
            query = query.filter(Distro._tags.any(DistroTag.tag.in_(tags)))
        query = query.join(DistroTree.arch).distinct()
        result = {}
        for osmajor, arch in query.values(OSMajor.osmajor, Arch.arch):
            result.setdefault(osmajor, []).append(arch)
        return result
예제 #14
0
파일: jobs.py 프로젝트: ustbgaofan/beaker
        if whiteboard:
            jobs = Job.by_whiteboard(whiteboard, jobs)

        jobs = Job.sanitise_jobs(jobs)

        if limit:
            limit = int(limit)
            jobs = jobs.limit(limit)

        jobs = jobs.values(Job.id)
        
        return_value = ['J:%s' % j[0] for j in jobs]
        return return_value

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def delete_jobs(self, jobs=None, tag=None, complete_days=None, family=None, dryrun=False, product=None):
        """
        delete_jobs will mark the job to be deleted

        To select jobs by id, pass an array for the *jobs* argument. Elements
        of the array must be strings of the form ``'J:123'``.
        Alternatively, pass some combination of the *tag*, *complete_days*, or
        *family* arguments to select jobs for deletion. These arguments behave
        as per the :meth:`jobs.list` method.

        If *dryrun* is True, deletions will be reported but nothing will be
        modified.

        Admins are not be able to delete jobs which are not owned by
        themselves by using the tag, complete_days etc kwargs, instead, they
예제 #15
0
class Recipes(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True

    hidden_id = widgets.HiddenField(name='id')
    confirm = widgets.Label(
        name='confirm', default="Are you sure you want to release the system?")
    return_reservation_form = widgets.TableForm(
        'end_recipe_reservation',
        fields=[hidden_id, confirm],
        action='./really_return_reservation',
        submit_text=_(u'Yes'))

    tasks = RecipeTasks()

    recipe_widget = RecipeWidget()

    log_types = dict(
        R=LogRecipe,
        T=LogRecipeTask,
        E=LogRecipeTaskResult,
    )

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def by_log_server(self, server, limit=50):
        """
        Returns a list of recipe IDs which have logs stored on the given 
        server. By default, returns at most 50 at a time.

        Only returns recipes where the whole recipe set has completed. Also 
        excludes recently completed recipe sets, since the system may continue 
        uploading logs for a short while until beaker-provision powers it off.
        """
        finish_threshold = datetime.utcnow() - timedelta(minutes=2)
        recipes = Recipe.query.join(Recipe.recipeset)\
                .filter(RecipeSet.status.in_([s for s in TaskStatus if s.finished]))\
                .filter(not_(RecipeSet.recipes.any(Recipe.finish_time >= finish_threshold)))\
                .filter(Recipe.log_server == server)\
                .limit(limit)
        return [recipe_id for recipe_id, in recipes.values(Recipe.id)]

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def register_file(self, server, recipe_id, path, filename, basepath):
        """
        register file and return path to store
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        if recipe.is_finished():
            raise BX('Cannot register file for finished recipe %s' %
                     recipe.t_id)

        # Add the log to the DB if it hasn't been recorded yet.
        log_recipe = LogRecipe.lazy_create(
            recipe_id=recipe.id,
            path=path,
            filename=filename,
        )
        log_recipe.server = server
        log_recipe.basepath = basepath
        # Pull log_server out of server_url.
        recipe.log_server = urlparse.urlparse(server)[1]
        return '%s' % recipe.filepath

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def files(self, recipe_id):
        """
        Return an array of logs for the given recipe.

        :param recipe_id: id of recipe
        :type recipe_id: integer

        .. deprecated:: 0.9.4
           Use :meth:`taskactions.files` instead.
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        return [log for log in recipe.all_logs]

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def change_files(self, recipe_id, server, basepath):
        """
        Change the server and basepath where the log files lives, Usually
         used to move from lab controller cache to archive storage.
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        for mylog in recipe.all_logs:
            myserver = '%s/%s/' % (server, mylog['filepath'])
            mybasepath = '%s/%s/' % (basepath, mylog['filepath'])
            self.change_file(mylog['tid'], myserver, mybasepath)
        recipe.log_server = urlparse.urlparse(server)[1]
        return True

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def change_file(self, tid, server, basepath):
        """
        Change the server and basepath where the log file lives, Usually
         used to move from lab controller cache to archive storage.
        """
        log_type, log_id = tid.split(":")
        if log_type.upper() in self.log_types.keys():
            try:
                mylog = self.log_types[log_type.upper()].by_id(log_id)
            except InvalidRequestError:
                raise BX(_("Invalid %s" % tid))
        mylog.server = server
        mylog.basepath = basepath
        return True

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def extend(self, recipe_id, kill_time):
        """
        Extend recipe watchdog by kill_time seconds
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        return recipe.extend(kill_time)

    @cherrypy.expose
    def console_output(self, recipe_id, output_length=None, offset=None):
        """
        Get text console log output from OpenStack 
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        manager = dynamic_virt.VirtManager(recipe.recipeset.job.owner)
        return manager.get_console_output(recipe.resource.instance_id,
                                          output_length)

    @cherrypy.expose
    def watchdog(self, recipe_id):
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        return recipe.status_watchdog()

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def stop(self, recipe_id, stop_type, msg=None):
        """
        Set recipe status to Completed
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        if stop_type not in recipe.stop_types:
            raise BX(
                _('Invalid stop_type: %s, must be one of %s' %
                  (stop_type, recipe.stop_types)))
        kwargs = dict(msg=msg)
        return getattr(recipe, stop_type)(**kwargs)

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def install_start(self, recipe_id=None):
        """
        Report comencement of provisioning of a recipe's resource, extend
        first task's watchdog, and report 'Install Started' against it.
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_("Invalid Recipe ID %s" % recipe_id))

        first_task = recipe.first_task
        if not recipe.resource.install_started:
            recipe.resource.install_started = datetime.utcnow()
            # extend watchdog by 3 hours 60 * 60 * 3
            kill_time = 10800
            # XXX In future releases where 'Provisioning'
            # is a valid recipe state, we will no longer
            # need the following block.
            log.debug('Extending watchdog for %s', first_task.t_id)
            first_task.extend(kill_time)
            log.debug('Recording /start for %s', first_task.t_id)
            first_task.pass_(path=u'/start',
                             score=0,
                             summary=u'Install Started')
            return True
        else:
            log.debug('Already recorded /start for %s', first_task.t_id)
            return False

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def postinstall_done(self, recipe_id=None):
        """
        Report completion of postinstallation
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_(u'Invalid Recipe ID %s' % recipe_id))
        recipe.resource.postinstall_finished = datetime.utcnow()
        return True

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def install_done(self, recipe_id=None, fqdn=None):
        """
        Report completion of installation with current FQDN
        """
        if not recipe_id:
            raise BX(_("No recipe id provided!"))
        if not fqdn:
            raise BX(_("No fqdn provided!"))

        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_("Invalid Recipe ID %s" % recipe_id))

        recipe.resource.install_finished = datetime.utcnow()
        # We don't want to change an existing FQDN, just set it
        # if it hasn't been set already (see BZ#879146)
        configured = recipe.resource.fqdn
        if configured is None:
            recipe.resource.fqdn = configured = fqdn
        elif configured != fqdn:
            # We use eager formatting here to make this easier to test
            log.info("Configured FQDN (%s) != reported FQDN (%s) in R:%s" %
                     (configured, fqdn, recipe_id))
        return configured

    @identity.require(identity.not_anonymous())
    @expose()
    def really_return_reservation(self, id, msg=None):
        try:
            recipe = Recipe.by_id(id)
        except InvalidRequestError:
            raise BX(_("Invalid Recipe ID %s" % id))
        recipe.return_reservation()

        flash(_(u"Successfully released reserved system for %s" % recipe.t_id))
        redirect('/jobs/mine')

    @expose(template="bkr.server.templates.form")
    @identity.require(identity.not_anonymous())
    def return_reservation(self, recipe_id=None):
        """
        End recipe reservation
        """
        if not recipe_id:
            raise BX(_("No recipe id provided!"))

        return dict(
            title='Release reserved system for Recipe %s' % recipe_id,
            form=self.return_reservation_form,
            action='./really_return_reservation',
            options={},
            value=dict(id=recipe_id),
        )

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def postreboot(self, recipe_id=None):
        # Backwards compat only, delete this after 0.10:
        # the recipe_id arg used to be hostname
        try:
            int(recipe_id)
        except ValueError:
            system = System.by_fqdn(recipe_id, identity.current.user)
            system.action_power('reboot', service=u'XMLRPC', delay=30)
            return system.fqdn

        try:
            recipe = Recipe.by_id(int(recipe_id))
        except (InvalidRequestError, NoResultFound, ValueError):
            raise BX(_('Invalid recipe ID %s') % recipe_id)
        if isinstance(recipe.resource, SystemResource):
            recipe.resource.system.action_power('reboot',
                                                service=u'XMLRPC',
                                                delay=30)
        return True

    @cherrypy.expose
    def to_xml(self, recipe_id=None):
        """ 
            Pass in recipe id and you'll get that recipe's xml
        """
        if not recipe_id:
            raise BX(_("No recipe id provided!"))
        try:
            recipexml = Recipe.by_id(recipe_id).to_xml().toprettyxml()
        except InvalidRequestError:
            raise BX(_("Invalid Recipe ID %s" % recipe_id))
        return recipexml

    def _recipe_search(self, recipe, **kw):
        recipe_search = search_utility.Recipe.search(recipe)
        for search in kw['recipesearch']:
            col = search['table']
            try:
                recipe_search.append_results(search['value'], col,
                                             search['operation'], **kw)
            except KeyError, e:
                log.error(e)
                return recipe_search.return_results()

        return recipe_search.return_results()
예제 #16
0
파일: jobs.py 프로젝트: ustbgaofan/beaker
    def filter(self, filters):
        """
        Returns a list of details for jobs filtered by the given criteria.

        The *filter* argument must be a an XML-RPC structure (dict) specifying
        filter criteria. The following keys are recognised:

            'tags'
                List of job tags.
            'daysComplete'
                Number of days elapsed since the jobs completion.
            'family'
                Job distro family, for example ``'RedHatEnterpriseLinuxServer5'``.
            'product'
                Job product name
            'owner'
                Job owner username
            'mine'
                Inclusion is equivalent to including own username in 'owner'
            'whiteboard'
                Job whiteboard
            'limit'
                Integer limit to number of jobs returned.
            'minid'
                Min JobID of the jobs to search
            'maxid'
                Maximum Job ID of the jobs to search

        Returns a two-element array. The first element is an array of JobIDs
        of the form ``'J:123'``, suitable to be passed to the
        :meth:`jobs.delete_jobs` method. The second element is a human-readable
        count of the number of Jobs matched. Does not return deleted jobs.
        """

        # if  min/max/both IDs have been specified, filter it right here
        minid = filters.get('minid', None)
        maxid = filters.get('maxid', None)
        jobs = session.query(Job)
        if minid:
            jobs = jobs.filter(Job.id >= minid)
        if maxid:
            jobs = jobs.filter(Job.id <= maxid)

        tags = filters.get('tags', None)
        complete_days = filters.get('daysComplete', None)
        family = filters.get('family', None)
        product = filters.get('product', None)
        owner = filters.get('owner', None)
        whiteboard = filters.get('whiteboard', None)
        mine = filters.get('mine', None)
        limit = filters.get('limit', None)

        if mine and not identity.not_anonymous():
            raise BX(_('You should be authenticated to use the --mine filter.'))

        if mine and identity.not_anonymous():
            if owner:
                if type(owner) is list:
                    owner.append(identity.current.user.user_name)
                else:
                    owner = [owner, identity.current.user.user_name]
            else:
                owner = identity.current.user.user_name

        jobs = jobs.order_by(Job.id.desc())
        if tags:
            jobs = Job.by_tag(tags, jobs)
        if complete_days:
            jobs = Job.complete_delta({'days':int(complete_days)}, jobs)
        if family:
            jobs = Job.has_family(family, jobs)
        if product:
            jobs = Job.by_product(product, jobs)
        if owner:
            jobs = Job.by_owner(owner, jobs)
        if whiteboard:
            jobs = Job.by_whiteboard(whiteboard, jobs)

        jobs = Job.sanitise_jobs(jobs)

        if limit:
            limit = int(limit)
            jobs = jobs.limit(limit)

        jobs = jobs.values(Job.id)
        
        return_value = ['J:%s' % j[0] for j in jobs]
        return return_value
예제 #17
0
class Auth(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True

    KRB_AUTH_PRINCIPAL = get("identity.krb_auth_principal")
    KRB_AUTH_KEYTAB = get("identity.krb_auth_keytab")

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def who_am_i(self):
        """
        Returns an XML-RPC structure (dict) with information about the 
        currently logged in user.
        Provided for testing purposes.

        .. versionadded:: 0.6.0
        .. versionchanged:: 0.6.1
           Formerly returned only the username.
        .. versionchanged:: 1.0
           Also return the email address of user.
        """
        retval = {'username': identity.current.user.user_name,
                  'email_address' : identity.current.user.email_address}
        if identity.current.proxied_by_user is not None:
            retval['proxied_by_username'] = identity.current.proxied_by_user.user_name
        return retval

    @cherrypy.expose
    def renew_session(self, *args, **kw):
        """
        Renew session, here to support the login method
        that was migrated from kobo.
        """
        if identity.current.anonymous:
            return True
        return False

    @cherrypy.expose
    def login_password(self, username, password, proxy_user=None):
        """
        Authenticates the current session using the given username and password.

        The caller may act as a proxy on behalf of another user by passing the 
        *proxy_user* argument. This requires that the caller has 'proxy_auth' 
        permission.

        :param proxy_user: username on whose behalf the caller is proxying
        :type proxy_user: string or None
        """
        user = User.by_user_name(username)
        if user is None:
            raise LoginException(_(u'Invalid username or password'))
        if not user.can_log_in():
            raise LoginException(_(u'Invalid username or password'))
        if not user.check_password(password):
            raise LoginException(_(u'Invalid username or password'))
        if proxy_user:
            if not user.has_permission(u'proxy_auth'):
                raise LoginException(_(u'%s does not have proxy_auth permission') % user.user_name)
            proxied_user = User.by_user_name(proxy_user)
            if proxied_user is None:
                raise LoginException(_(u'Proxy user %s does not exist') % proxy_user)
            identity.set_authentication(proxied_user, proxied_by=user)
        else:
            identity.set_authentication(user)
        return user.user_name

    @cherrypy.expose
    def login_krbV(self, krb_request, proxy_user=None):
        """
        Authenticates the current session using Kerberos.

        The caller may act as a proxy on behalf of another user by passing the 
        *proxy_user* argument. This requires that the caller has 'proxy_auth' 
        permission.
        
        :param krb_request: KRB_AP_REQ message containing client credentials, 
            as produced by :c:func:`krb5_mk_req`
        :type krb_request: base64-encoded string
        :param proxy_user: username on whose behalf the caller is proxying
        :type proxy_user: string or None

        This method is also available under the alias :meth:`auth.login_krbv`, 
        for compatibility with `Kobo`_.
        """
        import krbV
        import base64

        context = krbV.default_context()
        server_principal = krbV.Principal(name=self.KRB_AUTH_PRINCIPAL, context=context)
        server_keytab = krbV.Keytab(name=self.KRB_AUTH_KEYTAB, context=context)
    
        auth_context = krbV.AuthContext(context=context)
        auth_context.flags = krbV.KRB5_AUTH_CONTEXT_DO_SEQUENCE | krbV.KRB5_AUTH_CONTEXT_DO_TIME
        auth_context.addrs = (socket.gethostbyname(cherrypy.request.remote_host), 0, cherrypy.request.remote_addr, 0)
    
        # decode and read the authentication request
        decoded_request = base64.decodestring(krb_request)
        auth_context, opts, server_principal, cache_credentials = context.rd_req(decoded_request, server=server_principal, keytab=server_keytab, auth_context=auth_context, options=krbV.AP_OPTS_MUTUAL_REQUIRED)
        cprinc = cache_credentials[2]
    
        # remove @REALM
        username = cprinc.name.split("@")[0]
        user = User.by_user_name(username)
        if user is None:
            raise LoginException(_(u'Invalid username'))
        if not user.can_log_in():
            raise LoginException(_(u'Invalid username'))
        if proxy_user:
            if not user.has_permission(u'proxy_auth'):
                raise LoginException(_(u'%s does not have proxy_auth permission') % user.user_name)
            proxied_user = User.by_user_name(proxy_user)
            if proxied_user is None:
                raise LoginException(_(u'Proxy user %s does not exist') % proxy_user)
            identity.set_authentication(proxied_user, proxied_by=user)
        else:
            identity.set_authentication(user)
        return username

    # Alias kerberos login
    login_krbv = login_krbV

    @cherrypy.expose
    def logout(self, *args):
        """
        Invalidates the current session.
        """
        identity.clear_authentication()
        return True
예제 #18
0
class Jobs(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True
    job_list_action_widget = JobActionWidget()
    job_page_action_widget = JobPageActionWidget()
    recipeset_widget = RecipeSetWidget()
    recipe_widget = RecipeWidget()
    priority_widget = PriorityWidget(
    )  #FIXME I have a feeling we don't need this as the RecipeSet widget declares an instance of it
    product_widget = ProductWidget()
    retention_tag_widget = RetentionTagWidget()
    job_type = {'RS': RecipeSet, 'J': Job}
    whiteboard_widget = JobWhiteboard()

    hidden_id = widgets.HiddenField(name='id')
    confirm = widgets.Label(name='confirm',
                            default="Are you sure you want to cancel?")
    message = widgets.TextArea(name='msg',
                               label=_(u'Reason?'),
                               help_text=_(u'Optional'))

    _upload = widgets.FileField(name='filexml', label='Job XML')
    form = HorizontalForm('jobs',
                          fields=[_upload],
                          action='save_data',
                          submit_text=_(u'Submit Data'))
    del _upload

    cancel_form = widgets.TableForm('cancel_job',
                                    fields=[hidden_id, message, confirm],
                                    action='really_cancel',
                                    submit_text=_(u'Yes'))

    job_form = JobForm()

    job_schema_doc = lxml.etree.parse(
        pkg_resources.resource_stream('bkr.common', 'schema/beaker-job.rng'))

    @classmethod
    def success_redirect(cls, id, url='/jobs/mine', *args, **kw):
        flash(_(u'Success! job id: %s' % id))
        redirect('%s' % url)

    @expose(template='bkr.server.templates.form-post')
    @identity.require(identity.not_anonymous())
    def new(self, **kw):
        return dict(
            title='New Job',
            form=self.form,
            action='./clone',
            options={},
            value=kw,
        )

    def _check_job_deletability(self, t_id, job):
        if not isinstance(job, Job):
            raise TypeError('%s is not of type %s' % (t_id, Job.__name__))
        if not job.can_delete(identity.current.user):
            raise BeakerException(
                _(u'You do not have permission to delete %s' % t_id))

    def _delete_job(self, t_id):
        job = TaskBase.get_by_t_id(t_id)
        self._check_job_deletability(t_id, job)
        Job.delete_jobs([job])
        return [t_id]

    @expose()
    @identity.require(identity.not_anonymous())
    @restrict_http_method('post')
    def delete_job_page(self, t_id):
        try:
            self._delete_job(t_id)
            flash(_(u'Succesfully deleted %s' % t_id))
        except (BeakerException, TypeError):
            flash(_(u'Unable to delete %s' % t_id))
            redirect('.')
        redirect('./mine')

    @expose()
    @identity.require(identity.not_anonymous())
    @restrict_http_method('post')
    def delete_job_row(self, t_id):
        try:
            self._delete_job(t_id)
            return [t_id]
        except (BeakerException, TypeError), e:
            log.debug(str(e))
            response.status = 400
            return ['Unable to delete %s' % t_id]
예제 #19
0
class Tasks(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True

    task_list_action_widget = TaskActionWidget()
    task_form = TaskSearchForm()
    task_widget = TasksWidget()

    _upload = widgets.FileField(name='task_rpm', label='Task RPM')
    form = HorizontalForm('task',
                          fields=[_upload],
                          action='save_data',
                          submit_text=_(u'Upload'))
    del _upload

    @expose(template='bkr.server.templates.form-post')
    @identity.require(identity.not_anonymous())
    def new(self, **kw):
        return_dict = dict(
            title='New Task',
            form=self.form,
            action='./save',
            options={},
            value=kw,
        )
        return return_dict

    @cherrypy.expose
    def filter(self, filter):
        """
        Returns a list of tasks filtered by the given criteria.

        The *filter* argument must be an XML-RPC structure (dict), with any of the following keys:

            'distro_name'
                Distro name. Include only tasks which are compatible 
                with this distro.
            'osmajor'
                OSVersion OSMajor, like RedHatEnterpriseLinux6.  Include only
                tasks which are compatible with this OSMajor.
            'names'
                Task name. Include only tasks that are named. Useful when
                combined with 'osmajor' or 'distro_name'.
            'packages'
                List of package names. Include only tasks which have a Run-For 
                entry matching any of these packages.
            'types'
                List of task types. Include only tasks which have one or more 
                of these types.
            'valid'
                bool 0 or 1. Include only tasks which are valid or not.
            'destructive'
                bool 0 or 1. Set to 0 for only non-destructive tasks. Set to 
                1 for only destructive tasks.

        The return value is an array of dicts, which are name and arches. 
        name is the name of the matching tasks.
        arches is an array of arches which this task does not apply for.
        Call :meth:`tasks.to_dict` to fetch metadata for a particular task.

        .. versionchanged:: 0.9
           Changed 'install_name' to 'distro_name' in the *filter* argument.
        """
        if filter.get('distro_name'):
            distro = Distro.by_name(filter['distro_name'])
            tasks = distro.tasks()
        elif 'osmajor' in filter and filter['osmajor']:
            try:
                osmajor = OSMajor.by_name(filter['osmajor'])
            except InvalidRequestError:
                raise BX(_('Invalid OSMajor: %s' % filter['osmajor']))
            tasks = osmajor.tasks()
        else:
            tasks = Task.query

        # Filter by valid task if requested
        if 'valid' in filter:
            tasks = tasks.filter(Task.valid == bool(filter['valid']))

        # Filter by destructive if requested
        if 'destructive' in filter:
            tasks = tasks.filter(
                Task.destructive == bool(filter['destructive']))

        # Filter by name if specified
        # /distribution/install, /distribution/reservesys
        if 'names' in filter and filter['names']:
            # if not a list, make it into a list.
            if isinstance(filter['names'], str):
                filter['names'] = [filter['names']]
            or_names = []
            for tname in filter['names']:
                or_names.append(Task.name == tname)
            tasks = tasks.filter(or_(*or_names))

        # Filter by packages if specified
        # apache, kernel, mysql, etc..
        if 'packages' in filter and filter['packages']:
            # if not a list, make it into a list.
            if isinstance(filter['packages'], str):
                filter['packages'] = [filter['packages']]
            tasks = tasks.filter(
                Task.runfor.any(
                    or_(*[
                        TaskPackage.package == package
                        for package in filter['packages']
                    ])))

        # Filter by type if specified
        # Tier1, Regression, KernelTier1, etc..
        if 'types' in filter and filter['types']:
            # if not a list, make it into a list.
            if isinstance(filter['types'], str):
                filter['types'] = [filter['types']]
            tasks = tasks.join('types')
            or_types = []
            for type in filter['types']:
                try:
                    tasktype = TaskType.by_name(type)
                except InvalidRequestError, err:
                    raise BX(_('Invalid Task Type: %s' % type))
                or_types.append(TaskType.id == tasktype.id)
            tasks = tasks.filter(or_(*or_types))

        # Return all task names
        return [
            dict(name=task.name,
                 arches=[str(arch.arch) for arch in task.excluded_arch])
            for task in tasks
        ]
예제 #20
0
class RecipeTasks(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def register_file(self, server, task_id, path, filename, basepath):
        """
        register file and return path to store
        """
        try:
            recipetask = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        if recipetask.is_finished():
            raise BX('Cannot register file for finished task %s' %
                     recipetask.t_id)

        # Add the log to the DB if it hasn't been recorded yet.
        log_recipe = LogRecipeTask.lazy_create(
            recipe_task_id=recipetask.id,
            path=path,
            filename=filename,
        )
        log_recipe.server = server
        log_recipe.basepath = basepath
        recipetask.recipe.log_server = urlparse.urlparse(server)[1]
        return '%s' % recipetask.filepath

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def register_result_file(self, server, result_id, path, filename,
                             basepath):
        """
        register file and return path to store
        """
        try:
            result = RecipeTaskResult.by_id(result_id)
        except InvalidRequestError:
            raise BX(_('Invalid result ID: %s' % result_id))
        if result.recipetask.is_finished():
            raise BX('Cannot register file for finished task %s' %
                     result.recipetask.t_id)

        log_recipe = LogRecipeTaskResult.lazy_create(
            recipe_task_result_id=result.id,
            path=path,
            filename=filename,
        )
        log_recipe.server = server
        log_recipe.basepath = basepath
        result.recipetask.recipe.log_server = urlparse.urlparse(server)[1]
        return '%s' % result.filepath

    @cherrypy.expose
    def watchdogs(self, status='active', lc=None):
        """ Return all active/expired tasks for this lab controller
            The lab controllers login with host/fqdn
        """
        # TODO work on logic that determines whether or not originator
        # was qpid or kobo ?
        if lc is None:
            try:
                labcontroller = identity.current.user.lab_controller
            except AttributeError:
                raise BX(
                    _('No lab controller passed in and not currently logged in'
                      ))

            if not labcontroller:
                raise BX(
                    _(u'Invalid login: %s, must log in as a lab controller' %
                      identity.current.user))
        else:
            try:
                labcontroller = LabController.by_name(lc)
            except InvalidRequestError:
                raise BX(_(u'Invalid lab controller: %s' % lc))

        return [
            dict(recipe_id=w.recipe.id,
                 system=w.recipe.resource.fqdn,
                 is_virt_recipe=(w.recipe.resource.type == ResourceType.virt))
            for w in Watchdog.by_status(labcontroller, status)
        ]

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def start(self, task_id, watchdog_override=None):
        """
        Set task status to Running
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        return task.start(watchdog_override)

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def extend(self, task_id, kill_time):
        """
        Extend tasks watchdog by kill_time seconds
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        return task.extend(kill_time)

    @cherrypy.expose
    def watchdog(self, task_id):
        """
        Returns number of seconds left on task_id watchdog, or False if it doesn't exist.
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        return task.status_watchdog()

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def stop(self, task_id, stop_type, msg=None):
        """
        Set task status to Completed
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        if stop_type not in task.stop_types:
            raise BX(
                _('Invalid stop_type: %s, must be one of %s' %
                  (stop_type, task.stop_types)))
        kwargs = dict(msg=msg)
        return getattr(task, stop_type)(**kwargs)

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def update(self, task_id, data):
        """
        XML-RPC method used by the lab controller harness API to update 
        a recipe-task's attributes.
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        if 'name' in data:
            task.name = data['name']
        if 'version' in data:
            task.version = data['version']
        return task.__json__()

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def result(self,
               task_id,
               result_type,
               path=None,
               score=None,
               summary=None):
        """
        Record a Result
        """
        try:
            task = RecipeTask.by_id(task_id)
        except InvalidRequestError:
            raise BX(_('Invalid task ID: %s' % task_id))
        if result_type not in task.result_types:
            raise BX(
                _('Invalid result_type: %s, must be one of %s' %
                  (result_type, task.result_types)))
        kwargs = dict(path=path, score=score, summary=summary)
        return getattr(task, result_type)(**kwargs)

    @expose(format='json')
    def to_xml(self, id):
        taskxml = RecipeTask.by_id(id).to_xml().toprettyxml()
        return dict(xml=taskxml)

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def peer_roles(self, task_id):
        try:
            task = RecipeTask.by_id(task_id)
        except NoResultFound:
            raise BX(_('Invalid task ID: %s') % task_id)
        # don't use set, we want to preserve ordering
        roles = {}
        for role, recipes in task.recipe.peer_roles().iteritems():
            fqdns = roles.setdefault(unicode(role), [])
            for recipe in recipes:
                if not recipe.resource or not recipe.resource.fqdn:
                    continue
                fqdn = unicode(recipe.resource.fqdn)
                if fqdn not in fqdns:
                    fqdns.append(fqdn)
        for role, tasks in task.peer_roles().iteritems():
            fqdns = roles.setdefault(unicode(role), [])
            for task in tasks:
                if not task.recipe.resource or not task.recipe.resource.fqdn:
                    continue
                fqdn = unicode(task.recipe.resource.fqdn)
                if fqdn not in fqdns:
                    fqdns.append(fqdn)
        return roles
예제 #21
0
class TaskActions(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True
    unstoppable_task_types = [Recipe, RecipeTaskResult]

    task_types = dict(J  = Job,
                      RS = RecipeSet,
                      R  = Recipe,
                      T  = RecipeTask,
                      TR = RecipeTaskResult)

    stoppable_task_types = dict([(rep, obj) for rep,obj in task_types.iteritems() if obj not in unstoppable_task_types])

    @cherrypy.expose
    def task_info(self, taskid,flat=True):
        """
        Returns an XML-RPC structure (dict) describing the current state of the 
        given job component.

        :param taskid: see above
        :type taskid: string
        """
        return TaskBase.get_by_t_id(taskid).task_info()

    @cherrypy.expose
    def to_xml(self, taskid, clone=False, exclude_enclosing_job=True, include_logs=True):
        """
        Returns an XML representation of the given job component, including its 
        current state.

        :param taskid: see above
        :type taskid: string
        :param clone: If True, returns XML suitable for submitting back to 
            Beaker. Otherwise, the XML includes results.
        :type clone: bool
        :param exclude_enclosing_job: If False, returns <job> as the root 
            element even when requesting a recipe set or recipe. This is useful 
            when cloning, in order to always produce a complete job definition.
        :type exclude_enclosing_job: bool
        :param include_logs: If True (the default), the results XML includes 
            links to all logs. This can make the results XML substantially larger.
        :type include_logs: bool
        """
        task_type, task_id = taskid.split(":")
        if task_type.upper() in self.task_types.keys():
            try:
                task = self.task_types[task_type.upper()].by_id(task_id)
            except InvalidRequestError:
                raise BX(_("Invalid %s %s" % (task_type, task_id)))
        return lxml.etree.tostring(
                task.to_xml(clone=clone,
                            include_enclosing_job=not exclude_enclosing_job,
                            include_logs=include_logs),
                xml_declaration=False, encoding='UTF-8')

    @cherrypy.expose
    def files(self, taskid):
        """
        Returns an array of XML-RPC structures (dicts) describing each of the 
        result files for the given job component and its descendants.

        :param taskid: see above
        :type taskid: string
        """
        return [l.dict for l in TaskBase.get_by_t_id(taskid).all_logs()]

    @identity.require(identity.not_anonymous())
    @cherrypy.expose
    def stop(self, taskid, stop_type, msg):
        """
        Cancels the given job. Note that when cancelling some part of a job 
        (for example, by passing *taskid* starting with ``R:`` to indicate 
        a particular recipe within a job) the entire job is cancelled.

        :param taskid: see above
        :type taskid: string
        :param stop_type: must be ``'cancel'`` (other values are reserved for 
            Beaker's internal use)
        :type stop_type: string
        :param msg: reason for cancelling
        :type msg: string
        """
        task_type, task_id = taskid.split(":")
        if task_type.upper() in self.stoppable_task_types.keys():
            try:
                task = self.stoppable_task_types[task_type.upper()].by_id(int(task_id))
            except (InvalidRequestError, ValueError):
                raise BX(_("Invalid %s %s" % (task_type, task_id)))
        else:
            raise BX(_("Task type %s is not stoppable" % (task_type)))
        if stop_type not in task.stop_types:
            raise BX(_('Invalid stop_type: %s, must be one of %s' %
                             (stop_type, task.stop_types)))
        if not task.can_stop(identity.current.user):
            raise BX(_("You don't have permission to %s %s" % (stop_type,
                                                               taskid)))
        kwargs = dict(msg = msg)
        task.record_activity(user=identity.current.user, service=u'XMLRPC',
                             field=u'Status', action=u'Cancelled', old='', new='')
        try:
            return getattr(task, stop_type)(**kwargs)
        except StaleTaskStatusException:
            raise BX(_(u"Could not cancel job id %s. Please try later" % task_id))
예제 #22
0
class Recipes(RPCRoot):
    # For XMLRPC methods in this class.
    exposed = True

    hidden_id = widgets.HiddenField(name='id')
    confirm = widgets.Label(
        name='confirm', default="Are you sure you want to release the system?")
    return_reservation_form = widgets.TableForm(
        'end_recipe_reservation',
        fields=[hidden_id, confirm],
        action='./really_return_reservation',
        submit_text=_(u'Yes'))

    tasks = RecipeTasks()

    recipe_widget = RecipeWidget()

    log_types = dict(
        R=LogRecipe,
        T=LogRecipeTask,
        E=LogRecipeTaskResult,
    )

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def by_log_server(self, server, limit=50):
        """
        Returns a list of recipe IDs which have logs stored on the given 
        server. By default, returns at most 50 at a time.

        Only returns recipes where the whole recipe set has completed. Also 
        excludes recently completed recipe sets, since the system may continue 
        uploading logs for a short while until beaker-provision powers it off.
        """
        finish_threshold = datetime.utcnow() - timedelta(minutes=2)
        recipes = Recipe.query.join(Recipe.recipeset)\
                .join(RecipeSet.job)\
                .filter(not_(Job.is_deleted))\
                .filter(RecipeSet.status.in_([s for s in TaskStatus if s.finished]))\
                .filter(not_(RecipeSet.recipes.any(Recipe.finish_time >= finish_threshold)))\
                .filter(Recipe.log_server == server)\
                .limit(limit)
        return [recipe_id for recipe_id, in recipes.values(Recipe.id)]

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def register_file(self, server, recipe_id, path, filename, basepath):
        """
        register file and return path to store
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        if recipe.is_finished():
            raise BX('Cannot register file for finished recipe %s' %
                     recipe.t_id)

        # Add the log to the DB if it hasn't been recorded yet.
        log_recipe = LogRecipe.lazy_create(
            recipe_id=recipe.id,
            path=path,
            filename=filename,
        )
        log_recipe.server = server
        log_recipe.basepath = basepath
        # Pull log_server out of server_url.
        recipe.log_server = urlparse.urlparse(server)[1]
        return '%s' % recipe.filepath

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def files(self, recipe_id):
        """
        Return an array of logs for the given recipe.

        :param recipe_id: id of recipe
        :type recipe_id: integer

        .. deprecated:: 0.9.4
           Use :meth:`taskactions.files() <bkr.server.task_actions.taskactions.files>` instead.
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        # Build a list of logs excluding duplicate paths, to mitigate:
        # https://bugzilla.redhat.com/show_bug.cgi?id=963492
        logdicts = []
        seen_paths = set()
        for log in recipe.all_logs():
            logdict = log.dict
            # The path we care about here is the path which beaker-transfer
            # will move the file to.
            # Don't be tempted to use os.path.join() here since log['path']
            # is often '/' which does not give the result you would expect.
            path = os.path.normpath(
                '%s/%s/%s' %
                (logdict['filepath'], logdict['path'], logdict['filename']))
            if path in seen_paths:
                logger.warn('%s contains duplicate log %s', log.parent.t_id,
                            path)
            else:
                seen_paths.add(path)
                logdicts.append(logdict)
        return logdicts

    @cherrypy.expose
    @identity.require(identity.in_group('lab_controller'))
    def change_files(self, recipe_id, server, basepath):
        """
        Change the server and basepath where the log files lives, Usually
         used to move from lab controller cache to archive storage.
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        for mylog in recipe.all_logs():
            mylog.server = '%s/%s/' % (server, mylog.parent.filepath)
            mylog.basepath = '%s/%s/' % (basepath, mylog.parent.filepath)
        recipe.log_server = urlparse.urlparse(server)[1]
        return True

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def extend(self, recipe_id, kill_time):
        """
        Extend recipe watchdog by kill_time seconds
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        return recipe.extend(kill_time)

    @cherrypy.expose
    def console_output(self, recipe_id, output_length=None, offset=None):
        """
        Get text console log output from OpenStack 
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        manager = dynamic_virt.VirtManager(recipe.recipeset.job.owner)
        return manager.get_console_output(recipe.resource.instance_id,
                                          output_length)

    @cherrypy.expose
    def watchdog(self, recipe_id):
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        return recipe.status_watchdog()

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def stop(self, recipe_id, stop_type, msg=None):
        """
        Set recipe status to Completed
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_('Invalid recipe ID: %s' % recipe_id))
        if stop_type not in recipe.stop_types:
            raise BX(
                _('Invalid stop_type: %s, must be one of %s' %
                  (stop_type, recipe.stop_types)))
        kwargs = dict(msg=msg)
        return getattr(recipe, stop_type)(**kwargs)

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def install_start(self, recipe_id=None):
        """
        Records the start of a recipe's installation. The watchdog is extended 
        by 3 hours to allow the installation to complete.
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_("Invalid Recipe ID %s" % recipe_id))
        if not recipe.installation:
            raise BX(_('Recipe %s not provisioned yet') % recipe_id)

        installation = recipe.installation
        if not installation.install_started:
            installation.install_started = datetime.utcnow()
            # extend watchdog by 3 hours 60 * 60 * 3
            kill_time = 10800
            logger.debug('Extending watchdog for %s', recipe.t_id)
            recipe.extend(kill_time)
            return True
        else:
            logger.debug('Already recorded install_started for %s',
                         recipe.t_id)
            return False

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def postinstall_done(self, recipe_id=None):
        """
        Report completion of postinstallation
        """
        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_(u'Invalid Recipe ID %s' % recipe_id))
        if not recipe.installation:
            raise BX(_('Recipe %s not provisioned yet') % recipe_id)
        recipe.installation.postinstall_finished = datetime.utcnow()
        return True

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def install_done(self, recipe_id=None, fqdn=None):
        """
        Report completion of installation with current FQDN
        """
        if not recipe_id:
            raise BX(_("No recipe id provided!"))

        try:
            recipe = Recipe.by_id(recipe_id)
        except InvalidRequestError:
            raise BX(_("Invalid Recipe ID %s" % recipe_id))
        if not recipe.installation:
            raise BX(_('Recipe %s not provisioned yet') % recipe_id)

        recipe.installation.install_finished = datetime.utcnow()
        # We don't want to change an existing FQDN, just set it
        # if it hasn't been set already (see BZ#879146)
        configured = recipe.resource.fqdn
        if configured is None and fqdn:
            recipe.resource.fqdn = configured = fqdn
        elif configured != fqdn:
            # We use eager formatting here to make this easier to test
            logger.info("Configured FQDN (%s) != reported FQDN (%s) in R:%s" %
                        (configured, fqdn, recipe_id))
        return configured

    @identity.require(identity.not_anonymous())
    @expose()
    def really_return_reservation(self, id, msg=None):
        try:
            recipe = Recipe.by_id(id)
        except InvalidRequestError:
            raise BX(_("Invalid Recipe ID %s" % id))
        recipe.return_reservation()

        flash(_(u"Successfully released reserved system for %s" % recipe.t_id))
        redirect('/jobs/mine')

    @expose(template="bkr.server.templates.form")
    @identity.require(identity.not_anonymous())
    def return_reservation(self, recipe_id=None):
        """
        End recipe reservation
        """
        if not recipe_id:
            raise BX(_("No recipe id provided!"))

        return dict(
            title='Release reserved system for Recipe %s' % recipe_id,
            form=self.return_reservation_form,
            action='./really_return_reservation',
            options={},
            value=dict(id=recipe_id),
        )

    @cherrypy.expose
    @identity.require(identity.not_anonymous())
    def postreboot(self, recipe_id=None):
        # Backwards compat only, delete this after 0.10:
        # the recipe_id arg used to be hostname
        try:
            int(recipe_id)
        except ValueError:
            system = System.by_fqdn(recipe_id, identity.current.user)
            system.action_power('reboot', service=u'XMLRPC', delay=30)
            return system.fqdn

        try:
            recipe = Recipe.by_id(int(recipe_id))
        except (InvalidRequestError, NoResultFound, ValueError):
            raise BX(_('Invalid recipe ID %s') % recipe_id)
        if isinstance(recipe.resource, SystemResource):
            recipe.resource.system.action_power('reboot',
                                                service=u'XMLRPC',
                                                delay=30)
        return True

    @cherrypy.expose
    def to_xml(self, recipe_id=None):
        """ 
            Pass in recipe id and you'll get that recipe's xml
        """
        if not recipe_id:
            raise BX(_("No recipe id provided!"))
        try:
            recipexml = etree.tostring(Recipe.by_id(recipe_id).to_xml(),
                                       pretty_print=True,
                                       encoding='utf8')
        except InvalidRequestError:
            raise BX(_("Invalid Recipe ID %s" % recipe_id))
        return recipexml

    def _recipe_search(self, recipe, **kw):
        recipe_search = search_utility.Recipe.search(recipe)
        for search in kw['recipesearch']:
            col = search['table']
            try:
                recipe_search.append_results(search['value'], col,
                                             search['operation'], **kw)
            except KeyError, e:
                logger.error(e)
                return recipe_search.return_results()

        return recipe_search.return_results()
예제 #23
0
class Groups(AdminPage):
    # For XMLRPC methods in this class.
    exposed = True
    group_id = widgets.HiddenField(name='group_id')
    auto_users = AutoCompleteField(name='user',
                                   search_controller=url("/users/by_name"),
                                   search_param="input",
                                   result_name="matches")
    auto_systems = AutoCompleteField(name='system',
                                     search_controller=url("/by_fqdn"),
                                     search_param="input",
                                     result_name="matches")

    search_groups = AutoCompleteField(
        name='group',
        search_controller=url("/groups/by_name?anywhere=1"),
        search_param="name",
        result_name="groups")

    search_permissions = AutoCompleteField(
        name='permissions',
        search_controller=url("/groups/get_permissions"),
        search_param="input",
        result_name="matches")

    group_form = GroupForm()

    permissions_form = InlineRemoteForm(
        'Permissions',
        fields=[search_permissions, group_id],
        submit_text=_(u'Add'),
        on_success='add_group_permission_success(http_request.responseText)',
        on_failure='add_group_permission_failure(http_request.responseText)',
        before='before_group_permission_submit()',
        after='after_group_permission_submit()',
    )

    group_user_form = InlineForm(
        'GroupUser',
        fields=[group_id, auto_users],
        action='save_data',
        submit_text=_(u'Add'),
    )

    group_system_form = InlineForm(
        'GroupSystem',
        fields=[group_id, auto_systems],
        action='save_data',
        submit_text=_(u'Add'),
    )

    delete_link = DeleteLinkWidgetForm()

    def __init__(self, *args, **kw):
        kw['search_url'] = url("/groups/by_name?anywhere=1")
        kw['search_name'] = 'group'
        kw['widget_action'] = ''
        super(Groups, self).__init__(*args, **kw)

        self.search_col = Group.group_name
        self.search_mapper = Group

    @expose(format='json')
    def by_name(self, input, *args, **kw):
        input = input.lower()
        if 'anywhere' in kw:
            search = Group.list_by_name(input, find_anywhere=True)
        else:
            search = Group.list_by_name(input)

        groups = [match.group_name for match in search]
        return dict(matches=groups)

    @expose(format='json')
    @identity.require(identity.not_anonymous())
    def remove_group_permission(self, group_id, permission_id):
        try:
            group = Group.by_id(group_id)
        except DatabaseLookupError:
            log.exception('Group id %s is not a valid Group to remove' %
                          group_id)
            return ['0']

        if not group.can_edit(identity.current.user):
            log.exception(
                'User %d does not have edit permissions for Group id %s' %
                (identity.current.user.user_id, group_id))
            response.status = 403
            return ['You are not an owner of group %s' % group]

        try:
            permission = Permission.by_id(permission_id)
        except NoResultFound:
            log.exception(
                'Permission id %s is not a valid Permission to remove' %
                permission_id)
            return ['0']
        group.permissions.remove(permission)
        return ['1']

    @expose(format='json')
    def get_permissions(self, input):
        results = Permission.by_name(input, anywhere=True)
        permission_names = [result.permission_name for result in results]
        return dict(matches=permission_names)

    @identity.require(identity.not_anonymous())
    @expose(template='bkr.server.templates.form')
    def new(self, **kw):
        return dict(
            form=self.group_form,
            title='New Group',
            action='./save_new',
            options={},
            value=kw,
        )

    def show_members(self, group):
        can_edit = False
        if identity.current.user:
            can_edit = group.can_modify_membership(identity.current.user)

        def show_ownership_status(member):
            is_owner = member.is_owner
            if can_edit:
                if is_owner:
                    return XML('<a class="btn change_ownership_remove" '
                               'href="revoke_owner?group_id=%s&amp;id=%s">'
                               '<i class="fa fa-times"/> Remove</a>' %
                               (group.group_id, member.user_id))
                else:
                    return XML('<a class="btn change_ownership_add" '
                               'href="grant_owner?group_id=%s&amp;id=%s">'
                               '<i class="fa fa-plus"/> Add</a>' %
                               (group.group_id, member.user_id))
            else:
                is_owner = 'Yes' if is_owner else 'No'
                return is_owner

        def remove_button(member):
            return XML(
                '<a class="btn" href="removeUser?group_id=%s&amp;id=%s">'
                '<i class="fa fa-times"/> Remove</a>' %
                (group.group_id, member.user_id))

        user_fields = [('User', lambda x: x.user.user_name)]

        user_fields.append(('Group Ownership', show_ownership_status))
        if can_edit:
            user_fields.append(('Group Membership', remove_button))

        return BeakerDataGrid(name='group_members_grid', fields=user_fields)

    @expose(template='bkr.server.templates.grid')
    @paginate('list', default_order='fqdn', limit=20, max_limit=None)
    def systems(self, group_id=None, *args, **kw):
        try:
            group = Group.by_id(group_id)
        except DatabaseLookupError:
            log.exception('Group id %s is not a valid group id' % group_id)
            flash(_(u'Need a valid group to search on'))
            redirect('../groups/mine')

        systems = System.all(identity.current.user). \
                  filter(System.groups.contains(group)). \
                  filter(System.status != SystemStatus.removed)
        title = 'Systems in Group %s' % group.group_name
        from bkr.server.controllers import Root
        return Root()._systems(systems, title, group_id=group_id, **kw)

    @expose(template='bkr.server.templates.group_form')
    def edit(self, group_id=None, group_name=None, **kw):
        # Not just for editing, also provides a read-only view
        if group_id is not None:
            try:
                group = Group.by_id(group_id)
            except DatabaseLookupError:
                log.exception('Group id %s is not a valid group id' % group_id)
                flash(_(u'Need a valid group to search on'))
                redirect('../groups/mine')
        elif group_name is not None:
            try:
                group = Group.by_name(group_name)
            except NoResultFound:
                log.exception('Group name %s is not a valid group name' %
                              group_name)
                flash(_(u'Need a valid group to search on'))
                redirect('../groups/mine')
        else:
            redirect('../groups/mine')

        usergrid = self.show_members(group)

        can_edit = False
        if identity.current.user:
            can_edit = group.can_edit(identity.current.user)

        systems_fields = [('System', lambda x: x.link)]
        if can_edit:
            system_remove_widget = DeleteLinkWidgetForm(
                action='removeSystem',
                hidden_fields=[
                    widgets.HiddenField(name='group_id'),
                    widgets.HiddenField(name='id')
                ],
                action_text=u'Remove')
            systems_fields.append((' ', lambda x: system_remove_widget.display(
                dict(group_id=group_id, id=x.id))))
        systemgrid = BeakerDataGrid(fields=systems_fields)

        permissions_fields = [('Permission', lambda x: x.permission_name)]
        if can_edit:
            permissions_fields.append((' ', lambda x: XML(
                '<a class="btn" href="#" id="remove_permission_%s">'
                '<i class="fa fa-times"/> Remove</a>' % x.permission_id)))
        group_permissions_grid = BeakerDataGrid(name='group_permission_grid',
                                                fields=permissions_fields)
        group_permissions = GroupPermissions()

        return dict(
            form=self.group_form,
            system_form=self.group_system_form,
            user_form=self.group_user_form,
            group_edit_js=LocalJSLink('bkr',
                                      '/static/javascript/group_users_v2.js'),
            action='./save',
            system_action='./save_system',
            user_action='./save_user',
            options={},
            value=group,
            group_pw=group.root_password,
            usergrid=usergrid,
            systemgrid=systemgrid,
            disabled_fields=[],
            group_permissions=group_permissions,
            group_form=self.permissions_form,
            group_permissions_grid=group_permissions_grid,
        )

    def _new_group(self, group_id, display_name, group_name, ldap,
                   root_password):
        user = identity.current.user
        if ldap and not user.is_admin():
            flash(_(u'Only admins can create LDAP groups'))
            redirect('.')
        try:
            Group.by_name(group_name)
        except NoResultFound:
            pass
        else:
            flash(_(u"Group %s already exists." % group_name))
            redirect(".")

        group = Group()
        session.add(group)
        group.record_activity(user=user,
                              service=u'WEBUI',
                              field=u'Group',
                              action=u'Created')
        group.display_name = display_name
        group.group_name = group_name
        group.ldap = ldap
        if group.ldap:
            group.refresh_ldap_members()
        group.root_password = root_password
        if not ldap:  # LDAP groups don't have owners
            group.user_group_assocs.append(UserGroup(user=user, is_owner=True))
            group.activity.append(
                GroupActivity(user,
                              service=u'WEBUI',
                              action=u'Added',
                              field_name=u'User',
                              old_value=None,
                              new_value=user.user_name))
            group.activity.append(
                GroupActivity(user,
                              service=u'WEBUI',
                              action=u'Added',
                              field_name=u'Owner',
                              old_value=None,
                              new_value=user.user_name))
        return group

    @expose()
    @validate(form=group_form)
    @error_handler(new)
    @identity.require(identity.not_anonymous())
    def save_new(self,
                 group_id=None,
                 display_name=None,
                 group_name=None,
                 ldap=False,
                 root_password=None,
                 **kwargs):
        # save_new() is needed because 'edit' is not a viable
        # error handler for new groups.
        self._new_group(group_id, display_name, group_name, ldap,
                        root_password)
        flash(_(u"OK"))
        redirect("mine")

    @expose()
    @validate(form=group_form)
    @error_handler(edit)
    @identity.require(identity.not_anonymous())
    def save(self,
             group_id=None,
             display_name=None,
             group_name=None,
             ldap=False,
             root_password=None,
             **kwargs):

        user = identity.current.user

        if ldap and not user.is_admin():
            flash(_(u'Only admins can create LDAP groups'))
            redirect('mine')

        try:
            group = Group.by_id(group_id)
        except DatabaseLookupError:
            flash(_(u"Group %s does not exist." % group_id))
            redirect('mine')

        try:
            Group.by_name(group_name)
        except NoResultFound:
            pass
        else:
            if group_name != group.group_name:
                flash(
                    _(u'Failed to update group %s: Group name already exists: %s'
                      % (group.group_name, group_name)))
                redirect('mine')

        if not group.can_edit(user):
            flash(_(u'You are not an owner of group %s' % group))
            redirect('../groups/mine')

        try:
            group.set_name(user, u'WEBUI', group_name)
            group.set_display_name(user, u'WEBUI', display_name)
            group.ldap = ldap
            group.set_root_password(user, u'WEBUI', root_password)
        except BeakerException, err:
            session.rollback()
            flash(_(u'Failed to update group %s: %s' %
                    (group.group_name, err)))
            redirect('.')

        flash(_(u"OK"))
        redirect("mine")