Custom Search

Friday, January 17, 2014

Flow of OpenStack Horizon Create Project action Part2

1)
horizon/horizon/workflows/base.py

a)
class Action(forms.Form):
    """
    An ``Action`` represents an atomic logical interaction you can have with
    the system. This is easier to understand with a conceptual example: in the
    context of a "launch instance" workflow, actions would include "naming
    the instance", "selecting an image", and ultimately "launching the
    instance".

    Because ``Actions`` are always interactive, they always provide form
    controls, and thus inherit from Django's ``Form`` class. However, they
    have some additional intelligence added to them:

    * ``Actions`` are aware of the permissions required to complete them.

    * ``Actions`` have a meta-level concept of "help text" which is meant to be
      displayed in such a way as to give context to the action regardless of
      where the action is presented in a site or workflow.

    * ``Actions`` understand how to handle their inputs and produce outputs,
      much like :class:`~horizon.forms.SelfHandlingForm` does now.

    ``Action`` classes may define the following attributes in a ``Meta``
    class within them:

    .. attribute:: name

        The verbose name for this action. Defaults to the name of the class.

    .. attribute:: slug

        A semi-unique slug for this action. Defaults to the "slugified" name
        of the class.

    .. attribute:: permissions

        A list of permission names which this action requires in order to be
        completed. Defaults to an empty list (``[]``).

    .. attribute:: help_text

        A string of simple help text to be displayed alongside the Action's
        fields.

    .. attribute:: help_text_template

        A path to a template which contains more complex help text to be
        displayed alongside the Action's fields. In conjunction with
        :meth:`~horizon.workflows.Action.get_help_text` method you can
        customize your help text template to display practically anything.
    """

    __metaclass__ = ActionMetaclass

    def __init__(self, request, context, *args, **kwargs):
        if request.method == "POST":
            super(Action, self).__init__(request.POST, initial=context)
        else:
            super(Action, self).__init__(initial=context)

        if not hasattr(self, "handle"):
            raise AttributeError("The action %s must define a handle method."
                                 % self.__class__.__name__)
        self.request = request
        self._populate_choices(request, context)
        self.required_css_class = 'required'

    def __unicode__(self):
        return force_unicode(self.name)

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

    def _populate_choices(self, request, context):
        for field_name, bound_field in self.fields.items():
            meth = getattr(self, "populate_%s_choices" % field_name, None)
            if meth is not None and callable(meth):
                bound_field.choices = meth(request, context)

    def get_help_text(self, extra_context=None):
        """ Returns the help text for this step. """
        text = ""
        extra_context = extra_context or {}
        if self.help_text_template:
            tmpl = template.loader.get_template(self.help_text_template)
            context = template.RequestContext(self.request, extra_context)
            text += tmpl.render(context)
        else:
            text += linebreaks(force_unicode(self.help_text))
        return safe(text)

    def add_error(self, message):
        """
        Adds an error to the Action's Step based on API issues.
        """
        self._get_errors()[NON_FIELD_ERRORS] = self.error_class([message])

    def handle(self, request, context):
        """
        Handles any requisite processing for this action. The method should
        return either ``None`` or a dictionary of data to be passed to
        :meth:`~horizon.workflows.Step.contribute`.

        Returns ``None`` by default, effectively making it a no-op.
        """
        return None


b)
class Step(object):
    """
    A step is a wrapper around an action which defines it's context in a
    workflow. It knows about details such as:

    * The workflow's context data (data passed from step to step).

    * The data which must be present in the context to begin this step (the
      step's dependencies).

    * The keys which will be added to the context data upon completion of the
      step.

    * The connections between this step's fields and changes in the context
      data (e.g. if that piece of data changes, what needs to be updated in
      this step).

    A ``Step`` class has the following attributes:

    .. attribute:: action

        The :class:`~horizon.workflows.Action` class which this step wraps.

    .. attribute:: depends_on

        A list of context data keys which this step requires in order to
        begin interaction.

    .. attribute:: contributes

        A list of keys which this step will contribute to the workflow's
        context data. Optional keys should still be listed, even if their
        values may be set to ``None``.

    .. attribute:: connections

        A dictionary which maps context data key names to lists of callbacks.
        The callbacks may be functions, dotted python paths to functions
        which may be imported, or dotted strings beginning with ``"self"``
        to indicate methods on the current ``Step`` instance.

    .. attribute:: before

        Another ``Step`` class. This optional attribute is used to provide
        control over workflow ordering when steps are dynamically added to
        workflows. The workflow mechanism will attempt to place the current
        step before the step specified in the attribute.

    .. attribute:: after

        Another ``Step`` class. This attribute has the same purpose as
        :meth:`~horizon.workflows.Step.before` except that it will instead
        attempt to place the current step after the given step.

    .. attribute:: help_text

        A string of simple help text which will be prepended to the ``Action``
        class' help text if desired.

    .. attribute:: template_name

        A path to a template which will be used to render this step. In
        general the default common template should be used. Default:
        ``"horizon/common/_workflow_step.html"``.

    .. attribute:: has_errors

        A boolean value which indicates whether or not this step has any
        errors on the action within it or in the scope of the workflow. This
        attribute will only accurately reflect this status after validation
        has occurred.

    .. attribute:: slug

        Inherited from the ``Action`` class.

    .. attribute:: name

        Inherited from the ``Action`` class.

    .. attribute:: permissions

        Inherited from the ``Action`` class.
    """
    action_class = None
    depends_on = ()
    contributes = ()
    connections = None
    before = None
    after = None
    help_text = ""
    template_name = "horizon/common/_workflow_step.html"

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

    def __unicode__(self):
        return force_unicode(self.name)

    def __init__(self, workflow):
        super(Step, self).__init__()
        self.workflow = workflow

        cls = self.__class__.__name__
        if not (self.action_class and issubclass(self.action_class, Action)):
            raise AttributeError("You must specify an action for %s." % cls)

        self.slug = self.action_class.slug
        self.name = self.action_class.name
        self.permissions = self.action_class.permissions
        self.has_errors = False
        self._handlers = {}

        if self.connections is None:
            # We want a dict, but don't want to declare a mutable type on the
            # class directly.
            self.connections = {}

        # Gather our connection handlers and make sure they exist.
        for key, handlers in self.connections.items():
            self._handlers[key] = []
            # TODO(gabriel): This is a poor substitute for broader handling
            if not isinstance(handlers, (list, tuple)):
                raise TypeError("The connection handlers for %s must be a "
                                "list or tuple." % cls)
            for possible_handler in handlers:
                if callable(possible_handler):
                    # If it's callable we know the function exists and is valid
                    self._handlers[key].append(possible_handler)
                    continue
                elif not isinstance(possible_handler, basestring):
                    return TypeError("Connection handlers must be either "
                                     "callables or strings.")
                bits = possible_handler.split(".")
                if bits[0] == "self":
                    root = self
                    for bit in bits[1:]:
                        try:
                            root = getattr(root, bit)
                        except AttributeError:
                            raise AttributeError("The connection handler %s "
                                                 "could not be found on %s."
                                                 % (possible_handler, cls))
                    handler = root
                elif len(bits) == 1:
                    # Import by name from local module not supported
                    raise ValueError("Importing a local function as a string "
                                     "is not supported for the connection "
                                     "handler %s on %s."
                                     % (possible_handler, cls))
                else:
                    # Try a general import
                    module_name = ".".join(bits[:-1])
                    try:
                        mod = import_module(module_name)
                        handler = getattr(mod, bits[-1])
                    except ImportError:
                        raise ImportError("Could not import %s from the "
                                          "module %s as a connection "
                                             "handler on %s."
                                             % (bits[-1], module_name, cls))
                    except AttributeError:
                        raise AttributeError("Could not import %s from the "
                                             "module %s as a connection "
                                             "handler on %s."
                                             % (bits[-1], module_name, cls))
                self._handlers[key].append(handler)

    @property
    def action(self):
        if not getattr(self, "_action", None):
            try:
                # Hook in the action context customization.
                workflow_context = dict(self.workflow.context)
                context = self.prepare_action_context(self.workflow.request,
                                                      workflow_context)
                self._action = self.action_class(self.workflow.request,
                                                 context)
            except Exception:
                LOG.exception("Problem instantiating action class.")
                raise
        return self._action

    def prepare_action_context(self, request, context):
        """
        Allows for customization of how the workflow context is passed to the
        action; this is the reverse of what "contribute" does to make the
        action outputs sane for the workflow. Changes to the context are not
        saved globally here. They are localized to the action.

        Simply returns the unaltered context by default.
        """
        return context

    def get_id(self):
        """ Returns the ID for this step. Suitable for use in HTML markup. """
        return "%s__%s" % (self.workflow.slug, self.slug)

    def _verify_contributions(self, context):
        for key in self.contributes:
            # Make sure we don't skip steps based on weird behavior of
            # POST query dicts.
            field = self.action.fields.get(key, None)
            if field and field.required and not context.get(key):
                context.pop(key, None)
        failed_to_contribute = set(self.contributes)
        failed_to_contribute -= set(context.keys())
        if failed_to_contribute:
            raise exceptions.WorkflowError("The following expected data was "
                                           "not added to the workflow context "
                                           "by the step %s: %s."
                                           % (self.__class__,
                                              failed_to_contribute))
        return True

    def contribute(self, data, context):
        """
        Adds the data listed in ``contributes`` to the workflow's shared
        context. By default, the context is simply updated with all the data
        returned by the action.

        Note that even if the value of one of the ``contributes`` keys is
        not present (e.g. optional) the key should still be added to the
        context with a value of ``None``.
        """
        if data:
            for key in self.contributes:
                context[key] = data.get(key, None)
        return context

    def render(self):
        """ Renders the step. """
        step_template = template.loader.get_template(self.template_name)
        extra_context = {"form": self.action,
                         "step": self}
        context = template.RequestContext(self.workflow.request, extra_context)
        return step_template.render(context)

    def get_help_text(self):
        """ Returns the help text for this step. """
        text = linebreaks(force_unicode(self.help_text))
        text += self.action.get_help_text()
        return safe(text)

    def add_error(self, message):
        """
        Adds an error to the Step based on API issues.
        """
        self.action.add_error(message)

    def has_required_fields(self):
        """
        Returns True if action contains any required fields
        """
        for key in self.contributes:
            field = self.action.fields.get(key, None)
            if (field and field.required):
                return True
        return False




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

###Action Classes or Form Classes
a)
class CreateProjectInfoAction(workflows.Action):
    # Hide the domain_id and domain_name by default
    domain_id = forms.CharField(label=_("Domain ID"),
                                required=False,
                                widget=forms.HiddenInput())
    domain_name = forms.CharField(label=_("Domain Name"),
                                  required=False,
                                  widget=forms.HiddenInput())
    name = forms.CharField(label=_("Name"))
    description = forms.CharField(widget=forms.widgets.Textarea(),
                                  label=_("Description"),
                                  required=False)
    enabled = forms.BooleanField(label=_("Enabled"),
                                 required=False,
                                 initial=True)

    def __init__(self, request, *args, **kwargs):
        super(CreateProjectInfoAction, self).__init__(request,
                                                      *args,
                                                      **kwargs)
        # For keystone V3, display the two fields in read-only
        if keystone.VERSIONS.active >= 3:
            readonlyInput = forms.TextInput(attrs={'readonly': 'readonly'})
            self.fields["domain_id"].widget = readonlyInput
            self.fields["domain_name"].widget = readonlyInput

    class Meta:
        name = _("Project Info")
        help_text = _("From here you can create a new "
                      "project to organize users.")


b)
class UpdateProjectMembersAction(workflows.MembershipAction):
    def __init__(self, request, *args, **kwargs):
        super(UpdateProjectMembersAction, self).__init__(request,
                                                         *args,
                                                         **kwargs)
        err_msg = _('Unable to retrieve user list. Please try again later.')
        # Use the domain_id from the project
        domain_id = self.initial.get("domain_id", None)
        project_id = ''
        if 'project_id' in self.initial:
            project_id = self.initial['project_id']

        # Get the default role
        try:
            default_role = api.keystone.get_default_role(self.request)
            # Default role is necessary to add members to a project
            if default_role is None:
                default = getattr(settings,
                                  "OPENSTACK_KEYSTONE_DEFAULT_ROLE", None)
                msg = _('Could not find default role "%s" in Keystone') % \
                        default
                raise exceptions.NotFound(msg)
        except Exception:
            exceptions.handle(self.request,
                              err_msg,
                              redirect=reverse(INDEX_URL))
        default_role_name = self.get_default_role_field_name()
        self.fields[default_role_name] = forms.CharField(required=False)
        self.fields[default_role_name].initial = default_role.id

        # Get list of available users
        all_users = []
        try:
            all_users = api.keystone.user_list(request,
                                               domain=domain_id)
        except Exception:
            exceptions.handle(request, err_msg)
        users_list = [(user.id, user.name) for user in all_users]

        # Get list of roles
        role_list = []
        try:
            role_list = api.keystone.role_list(request)
        except Exception:
            exceptions.handle(request,
                              err_msg,
                              redirect=reverse(INDEX_URL))
        for role in role_list:
            field_name = self.get_member_field_name(role.id)
            label = role.name
            self.fields[field_name] = forms.MultipleChoiceField(required=False,
                                                                label=label)
            self.fields[field_name].choices = users_list
            self.fields[field_name].initial = []

        # Figure out users & roles
        if project_id:
            try:
                project_members = api.keystone.user_list(request,
                    project=project_id)
            except Exception:
                exceptions.handle(request, err_msg)

            for user in project_members:
                try:
                    roles = api.keystone.roles_for_user(self.request,
                                                        user.id,
                                                        project_id)
                except Exception:
                    exceptions.handle(request,
                                      err_msg,
                                      redirect=reverse(INDEX_URL))
                for role in roles:
                    field_name = self.get_member_field_name(role.id)
                    self.fields[field_name].initial.append(user.id)

    class Meta:
        name = _("Project Members")
        slug = PROJECT_USER_MEMBER_SLUG

c)
class UpdateProjectQuotaAction(workflows.Action):
    ifcb_label = _("Injected File Content Bytes")
    metadata_items = forms.IntegerField(min_value=-1,
                                        label=_("Metadata Items"))
    cores = forms.IntegerField(min_value=-1, label=_("VCPUs"))
    instances = forms.IntegerField(min_value=-1, label=_("Instances"))
    injected_files = forms.IntegerField(min_value=-1,
                                        label=_("Injected Files"))
    injected_file_content_bytes = forms.IntegerField(min_value=-1,
                                                     label=ifcb_label)
    volumes = forms.IntegerField(min_value=-1, label=_("Volumes"))
    snapshots = forms.IntegerField(min_value=-1, label=_("Snapshots"))
    gigabytes = forms.IntegerField(min_value=-1, label=_("Gigabytes"))
    ram = forms.IntegerField(min_value=-1, label=_("RAM (MB)"))
    floating_ips = forms.IntegerField(min_value=-1, label=_("Floating IPs"))
    fixed_ips = forms.IntegerField(min_value=-1, label=_("Fixed IPs"))
    security_groups = forms.IntegerField(min_value=-1,
                                         label=_("Security Groups"))
    security_group_rules = forms.IntegerField(min_value=-1,
                                              label=_("Security Group Rules"))

    # Neutron
    security_group = forms.IntegerField(min_value=-1,
                                        label=_("Security Groups"))
    security_group_rule = forms.IntegerField(min_value=-1,
                                             label=_("Security Group Rules"))
    floatingip = forms.IntegerField(min_value=-1, label=_("Floating IPs"))
    network = forms.IntegerField(min_value=-1, label=_("Networks"))
    port = forms.IntegerField(min_value=-1, label=_("Ports"))
    router = forms.IntegerField(min_value=-1, label=_("Routers"))
    subnet = forms.IntegerField(min_value=-1, label=_("Subnets"))

    def __init__(self, request, *args, **kwargs):
        super(UpdateProjectQuotaAction, self).__init__(request,
                                                       *args,
                                                       **kwargs)
        disabled_quotas = quotas.get_disabled_quotas(request)
        for field in disabled_quotas:
            if field in self.fields:
                self.fields[field].required = False
                self.fields[field].widget = forms.HiddenInput()

    class Meta:
        name = _("Quota")
        slug = 'update_quotas'
        help_text = _("From here you can set quotas "
                      "(max limits) for the project.")



###Step Classes or wrapper of Action classes
a)
class CreateProjectInfo(workflows.Step):
    action_class = CreateProjectInfoAction
    contributes = ("domain_id",
                   "domain_name",
                   "project_id",
                   "name",
                   "description",
                   "enabled")

b)
class UpdateProjectMembers(workflows.UpdateMembersStep):
    action_class = UpdateProjectMembersAction
    available_list_title = _("All Users")
    members_list_title = _("Project Members")
    no_available_text = _("No users found.")
    no_members_text = _("No users.")

    def contribute(self, data, context):
        if data:
            try:
                roles = api.keystone.role_list(self.workflow.request)
            except Exception:
                exceptions.handle(self.workflow.request,
                                  _('Unable to retrieve user list.'))

            post = self.workflow.request.POST
            for role in roles:
                field = self.get_member_field_name(role.id)
                context[field] = post.getlist(field)
        return context

c)
class UpdateProjectQuota(workflows.Step):
    action_class = UpdateProjectQuotaAction
    depends_on = ("project_id",)
    contributes = quotas.QUOTA_FIELDS


No comments:

Post a Comment