Custom Search

Friday, January 17, 2014

Flow of OpenStack Horizon Create Project action Part1

1)
horizon/horizon/utils/html.py

from django.forms.util import flatatt  # noqa

class HTMLElement(object):
    """ A generic base class that gracefully handles html-style attributes. """
    def __init__(self):
        self.attrs = getattr(self, "attrs", {})
        self.classes = getattr(self, "classes", [])

    def get_default_classes(self):
        """
        Returns an iterable of default classes which should be combined with
        any other declared classes.
        """
        return []

    def get_default_attrs(self):
        """
        Returns a dict of default attributes which should be combined with
        other declared attributes.
        """
        return {}

    def get_final_attrs(self):
        """
        Returns a dict containing the final attributes of this element
        which will be rendered.
        """
        final_attrs = copy.copy(self.get_default_attrs())
        final_attrs.update(self.attrs)
        # Handle css class concatenation
        default = " ".join(self.get_default_classes())
        defined = self.attrs.get('class', '')
        additional = " ".join(getattr(self, "classes", []))
        non_empty = [test for test in (defined, default, additional) if test]
        final_classes = " ".join(non_empty).strip()
        final_attrs.update({'class': final_classes})
        return final_attrs

    @property
    def attr_string(self):
        """
        Returns a flattened string of HTML attributes based on the
        ``attrs`` dict provided to the class.
        """
        return flatatt(self.get_final_attrs())

    @property
    def class_string(self):
        """
        Returns a list of class name of HTML Element in string
        """
        classes_str = " ".join(self.classes)
        return classes_str


2)
horizon/horizon/workflows/base.py


from horizon.utils import html

class Workflow(html.HTMLElement):
    """
    A Workflow is a collection of Steps. It's interface is very
    straightforward, but it is responsible for handling some very
    important tasks such as:

    * Handling the injection, removal, and ordering of arbitrary steps.

    * Determining if the workflow can be completed by a given user at runtime
      based on all available information.

    * Dispatching connections between steps to ensure that when context data
      changes all the applicable callback functions are executed.

    * Verifying/validating the overall data integrity and subsequently
      triggering the final method to complete the workflow.

    The ``Workflow`` class has the following attributes:

    .. attribute:: name

        The verbose name for this workflow which will be displayed to the user.
        Defaults to the class name.

    .. attribute:: slug

        The unique slug for this workflow. Required.

    .. attribute:: steps

        Read-only access to the final ordered set of step instances for
        this workflow.

    .. attribute:: default_steps

        A list of :class:`~horizon.workflows.Step` classes (Step class is a wrapper for modified Form/Action class) which serve as the
        starting point for this workflow's ordered steps. Defaults to an empty
        list (``[]``).

    .. attribute:: finalize_button_name

        The name which will appear on the submit button for the workflow's
        form. Defaults to ``"Save"``.

    .. attribute:: success_message

        A string which will be displayed to the user upon successful completion
        of the workflow. Defaults to
        ``"{{ workflow.name }} completed successfully."``

    .. attribute:: failure_message

        A string which will be displayed to the user upon failure to complete
        the workflow. Defaults to ``"{{ workflow.name }} did not complete."``

    .. attribute:: depends_on

        A roll-up list of all the ``depends_on`` values compiled from the
        workflow's steps.

    .. attribute:: contributions

        A roll-up list of all the ``contributes`` values compiled from the
        workflow's steps.

    .. attribute:: template_name

        Path to the template which should be used to render this workflow.
        In general the default common template should be used. Default:
        ``"horizon/common/_workflow.html"``.

    .. attribute:: entry_point

        The slug of the step which should initially be active when the
        workflow is rendered. This can be passed in upon initialization of
        the workflow, or set anytime after initialization but before calling
        either ``get_entry_point`` or ``render``.

    .. attribute:: redirect_param_name

        The name of a parameter used for tracking the URL to redirect to upon
        completion of the workflow. Defaults to ``"next"``.

    .. attribute:: object

        The object (if any) which this workflow relates to. In the case of
        a workflow which creates a new resource the object would be the created
        resource after the relevant creation steps have been undertaken. In
        the case of a workflow which updates a resource it would be the
        resource being updated after it has been retrieved.

    """
    __metaclass__ = WorkflowMetaclass
    slug = None
    default_steps = ()
    template_name = "horizon/common/_workflow.html"
    finalize_button_name = _("Save")
    success_message = _("%s completed successfully.")
    failure_message = _("%s did not complete.")
    redirect_param_name = "next"
    multipart = False
    _registerable_class = Step

    def __unicode__(self):
        return self.name

    def __repr__(self):
        return "<%s: %s>" % (self.__class__.__name__, self.slug)

    def __init__(self, request=None, context_seed=None, entry_point=None,
                 *args, **kwargs):
        super(Workflow, self).__init__(*args, **kwargs)
        if self.slug is None:
            raise AttributeError("The workflow %s must have a slug."
                                 % self.__class__.__name__)
        self.name = getattr(self, "name", self.__class__.__name__)
        self.request = request
        self.depends_on = set([])
        self.contributions = set([])
        self.entry_point = entry_point
        self.object = None

        # Put together our steps in order. Note that we pre-register
        # non-default steps so that we can identify them and subsequently
        # insert them in order correctly.
        self._registry = dict([(step_class, step_class(self)) for step_class
                               in self.__class__._cls_registry
                               if step_class not in self.default_steps])
        self._gather_steps()

        # Determine all the context data we need to end up with.
        for step in self.steps:
            self.depends_on = self.depends_on | set(step.depends_on)
            self.contributions = self.contributions | set(step.contributes)

        # Initialize our context. For ease we can preseed it with a
        # regular dictionary. This should happen after steps have been
        # registered and ordered.
        self.context = WorkflowContext(self)
        context_seed = context_seed or {}
        clean_seed = dict([(key, val)
                           for key, val in context_seed.items()
                           if key in self.contributions | self.depends_on])
        self.context_seed = clean_seed
        self.context.update(clean_seed)

        if request and request.method == "POST":
            for step in self.steps:
                valid = step.action.is_valid()
                # Be sure to use the CLEANED data if the workflow is valid.
                if valid:
                    data = step.action.cleaned_data
                else:
                    data = request.POST
                self.context = step.contribute(data, self.context)

    @property
    def steps(self):
        if getattr(self, "_ordered_steps", None) is None:
            self._gather_steps()
        return self._ordered_steps

    def get_step(self, slug):
        """ Returns the instantiated step matching the given slug. """
        for step in self.steps:
            if step.slug == slug:
                return step

    def _gather_steps(self):
        ordered_step_classes = self._order_steps()
        for default_step in self.default_steps:
            self.register(default_step)
            self._registry[default_step] = default_step(self)
        self._ordered_steps = [self._registry[step_class]
                               for step_class in ordered_step_classes
                               if has_permissions(self.request.user,
                                          self._registry[step_class])]

    def _order_steps(self):
        steps = list(copy.copy(self.default_steps))
        additional = self._registry.keys()
        for step in additional:
            try:
                min_pos = steps.index(step.after)
            except ValueError:
                min_pos = 0
            try:
                max_pos = steps.index(step.before)
            except ValueError:
                max_pos = len(steps)
            if min_pos > max_pos:
                raise exceptions.WorkflowError("The step %(new)s can't be "
                                               "placed between the steps "
                                               "%(after)s and %(before)s; the "
                                               "step %(before)s comes before "
                                               "%(after)s."
                                               % {"new": additional,
                                                  "after": step.after,
                                                  "before": step.before})
            steps.insert(max_pos, step)
        return steps

    def get_entry_point(self):
        """
        Returns the slug of the step which the workflow should begin on.

        This method takes into account both already-available data and errors
        within the steps.
        """
        # If we have a valid specified entry point, use it.
        if self.entry_point:
            if self.get_step(self.entry_point):
                return self.entry_point
        # Otherwise fall back to calculating the appropriate entry point.
        for step in self.steps:
            if step.has_errors:
                return step.slug
            try:
                step._verify_contributions(self.context)
            except exceptions.WorkflowError:
                return step.slug
        # If nothing else, just return the first step.
        return self.steps[0].slug

    def _trigger_handlers(self, key):
        responses = []
        handlers = [(step.slug, f) for step in self.steps
                                   for f in step._handlers.get(key, [])]
        for slug, handler in handlers:
            responses.append((slug, handler(self.request, self.context)))
        return responses

    @classmethod
    def register(cls, step_class):
        """ Registers a :class:`~horizon.workflows.Step` with the workflow. """
        if not inspect.isclass(step_class):
            raise ValueError('Only classes may be registered.')
        elif not issubclass(step_class, cls._registerable_class):
            raise ValueError('Only %s classes or subclasses may be registered.'
                             % cls._registerable_class.__name__)
        if step_class in cls._cls_registry:
            return False
        else:
            cls._cls_registry.add(step_class)
            return True

    @classmethod
    def unregister(cls, step_class):
        """
        Unregisters a :class:`~horizon.workflows.Step` from the workflow.
        """
        try:
            cls._cls_registry.remove(step_class)
        except KeyError:
            raise base.NotRegistered('%s is not registered' % cls)
        return cls._unregister(step_class)

    def validate(self, context):
        """
        Hook for custom context data validation. Should return a boolean
        value or raise :class:`~horizon.exceptions.WorkflowValidationError`.
        """
        return True

    def is_valid(self):
        """
        Verified that all required data is present in the context and
        calls the ``validate`` method to allow for finer-grained checks
        on the context data.
        """
        missing = self.depends_on - set(self.context.keys())
        if missing:
            raise exceptions.WorkflowValidationError(
                "Unable to complete the workflow. The values %s are "
                "required but not present." % ", ".join(missing))

        # Validate each step. Cycle through all of them to catch all errors
        # in one pass before returning.
        steps_valid = True
        for step in self.steps:
            if not step.action.is_valid():
                steps_valid = False
                step.has_errors = True
        if not steps_valid:
            return steps_valid
        return self.validate(self.context)

    def finalize(self):
        """
        Finalizes a workflow by running through all the actions in order
        and calling their ``handle`` methods. Returns ``True`` on full success,
        or ``False`` for a partial success, e.g. there were non-critical
        errors. (If it failed completely the function wouldn't return.)
        """
        partial = False
        for step in self.steps:
            try:
                data = step.action.handle(self.request, self.context)
                if data is True or data is None:
                    continue
                elif data is False:
                    partial = True
                else:
                    self.context = step.contribute(data or {}, self.context)
            except Exception:
                partial = True
                exceptions.handle(self.request)
        if not self.handle(self.request, self.context):
            partial = True
        return not partial

    def handle(self, request, context):
        """
        Handles any final processing for this workflow. Should return a boolean
        value indicating success.
        """
        return True

    def get_success_url(self):
        """
        Returns a URL to redirect the user to upon completion. By default it
        will attempt to parse a ``success_url`` attribute on the workflow,
        which can take the form of a reversible URL pattern name, or a
        standard HTTP URL.
        """
        try:
            return urlresolvers.reverse(self.success_url)
        except urlresolvers.NoReverseMatch:
            return self.success_url

    def format_status_message(self, message):
        """
        Hook to allow customization of the message returned to the user
        upon successful or unsuccessful completion of the workflow.

        By default it simply inserts the workflow's name into the message
        string.
        """
        if "%s" in message:
            return message % self.name
        else:
            return message

    def render(self):
        """ Renders the workflow. """
        workflow_template = template.loader.get_template(self.template_name)
        extra_context = {"workflow": self}
        if self.request.is_ajax():
            extra_context['modal'] = True
        context = template.RequestContext(self.request, extra_context)
        return workflow_template.render(context)

    def get_absolute_url(self):
        """ Returns the canonical URL for this workflow.

        This is used for the POST action attribute on the form element
        wrapping the workflow.

        For convenience it defaults to the value of
        ``request.get_full_path()`` with any query string stripped off,
        e.g. the path at which the workflow was requested.
        """
        return self.request.get_full_path().partition('?')[0]

    def add_error_to_step(self, message, slug):
        """
        Adds an error to the workflow's Step with the
        specifed slug based on API issues. This is useful
        when you wish for API errors to appear as errors on
        the form rather than using the messages framework.
        """
        step = self.get_step(slug)
        if step:
            step.add_error(message)



3)
horizon/horizon/workflows/__init__.py

from horizon.workflows.base import Workflow  # noqa
assert Workflow


4)
horizon/openstack_dashboard/dashboards/admin/projects/workflows.py


from horizon import workflows

class CreateProject(workflows.Workflow):
    slug = "create_project"
    name = _("Create Project")
    finalize_button_name = _("Create Project")
    success_message = _('Created new project "%s".')
    failure_message = _('Unable to create project "%s".')
    success_url = "horizon:admin:projects:index"
    default_steps = (CreateProjectInfo,
                     UpdateProjectMembers,
                     UpdateProjectQuota)

    def __init__(self, request=None, context_seed=None, entry_point=None,
                 *args, **kwargs):
        if PROJECT_GROUP_ENABLED:
            self.default_steps = (CreateProjectInfo,
                                  UpdateProjectMembers,
                                  UpdateProjectGroups,
                                  UpdateProjectQuota)
        super(CreateProject, self).__init__(request=request,
                                            context_seed=context_seed,
                                            entry_point=entry_point,
                                            *args,
                                            **kwargs)

    def format_status_message(self, message):
        return message % self.context.get('name', 'unknown project')

    def handle(self, request, data):
        # create the project
        domain_id = data['domain_id']
        try:
            desc = data['description']
            self.object = api.keystone.tenant_create(request,
                                                     name=data['name'],
                                                     description=desc,
                                                     enabled=data['enabled'],
                                                     domain=domain_id)
        except Exception:
            exceptions.handle(request, ignore=True)
            return False

        project_id = self.object.id

        # update project members
        users_to_add = 0
        try:
            available_roles = api.keystone.role_list(request)
            member_step = self.get_step(PROJECT_USER_MEMBER_SLUG)
            # count how many users are to be added
            for role in available_roles:
                field_name = member_step.get_member_field_name(role.id)
                role_list = data[field_name]
                users_to_add += len(role_list)
            # add new users to project
            for role in available_roles:
                field_name = member_step.get_member_field_name(role.id)
                role_list = data[field_name]
                users_added = 0
                for user in role_list:
                    api.keystone.add_tenant_user_role(request,
                                                      project=project_id,
                                                      user=user,
                                                      role=role.id)
                    users_added += 1
                users_to_add -= users_added
        except Exception:
            if PROJECT_GROUP_ENABLED:
                group_msg = _(", add project groups")
            else:
                group_msg = ""
            exceptions.handle(request, _('Failed to add %(users_to_add)s '
                                         'project members%(group_msg)s and '
                                         'set project quotas.')
                                      % {'users_to_add': users_to_add,
                                         'group_msg': group_msg})

        if PROJECT_GROUP_ENABLED:
            # update project groups
            groups_to_add = 0
            try:
                available_roles = api.keystone.role_list(request)
                member_step = self.get_step(PROJECT_GROUP_MEMBER_SLUG)

                # count how many groups are to be added
                for role in available_roles:
                    field_name = member_step.get_member_field_name(role.id)
                    role_list = data[field_name]
                    groups_to_add += len(role_list)
                # add new groups to project
                for role in available_roles:
                    field_name = member_step.get_member_field_name(role.id)
                    role_list = data[field_name]
                    groups_added = 0
                    for group in role_list:
                        api.keystone.add_group_role(request,
                                                    role=role.id,
                                                    group=group,
                                                    project=project_id)
                        groups_added += 1
                    groups_to_add -= groups_added
            except Exception:
                exceptions.handle(request, _('Failed to add %s project groups '
                                             'and update project quotas.'
                                             % groups_to_add))

        # Update the project quota.
        nova_data = dict(
            [(key, data[key]) for key in quotas.NOVA_QUOTA_FIELDS])
        try:
            nova.tenant_quota_update(request, project_id, **nova_data)

            if base.is_service_enabled(request, 'volume'):
                cinder_data = dict([(key, data[key]) for key in
                                    quotas.CINDER_QUOTA_FIELDS])
                cinder.tenant_quota_update(request,
                                           project_id,
                                           **cinder_data)

            if api.base.is_service_enabled(request, 'network') and \
                    api.neutron.is_quotas_extension_supported(request):
                neutron_data = dict([(key, data[key]) for key in
                                     quotas.NEUTRON_QUOTA_FIELDS])
                api.neutron.tenant_quota_update(request,
                                                project_id,
                                                **neutron_data)
        except Exception:
            exceptions.handle(request, _('Unable to set project quotas.'))
        return True

No comments:

Post a Comment