/usr/share/pyshared/maasserver/node_action.py is in python-django-maas 1.2+bzr1373+dfsg-0ubuntu1~12.04.6.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | # Copyright 2012 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Node actions.
These are actions that appear as buttons no the UI's Node page, depending
on the node's state, the user's privileges etc.
To define a new node action, derive a class for it from :class:`NodeAction`,
provide the missing pieces documented in the class, and add it to
`ACTION_CLASSES`. The actions will always appear on the page in the same
order as they do in `ACTION_CLASSES`.
"""
from __future__ import (
absolute_import,
print_function,
unicode_literals,
)
__metaclass__ = type
__all__ = [
'compile_node_actions',
]
from abc import (
ABCMeta,
abstractmethod,
abstractproperty,
)
from collections import OrderedDict
from textwrap import dedent
from django.core.urlresolvers import reverse
from maasserver.enum import (
NODE_PERMISSION,
NODE_STATUS,
NODE_STATUS_CHOICES_DICT,
)
from maasserver.exceptions import Redirect
from maasserver.models import (
Node,
SSHKey,
)
# All node statuses.
ALL_STATUSES = set(NODE_STATUS_CHOICES_DICT.keys())
class NodeAction:
"""Base class for node actions."""
__metaclass__ = ABCMeta
display = abstractproperty("""
Action name.
Will be used as the label for the action's button.
""")
actionable_statuses = abstractproperty("""
Node states for which this action makes sense.
A collection of NODE_STATUS values. The action will be available
only if `node.status in action.actionable_statuses`.
""")
permission = abstractproperty("""
Required permission.
A NODE_PERMISSION value. The action will be available only if the
user has this given permission on the subject node.
""")
def __init__(self, node, user, request=None):
"""Initialize a node action.
All node actions' initializers must accept these same arguments,
without variations.
"""
self.node = node
self.user = user
self.request = request
def inhibit(self):
"""Overridable: is there any reason not to offer this action?
This property may return a reason to inhibit this action, in which
case its button may still be visible in the UI, but disabled. A
tooltip will provide the reason, as returned by this method.
:return: A human-readable reason to inhibit the action, or None if
the action is valid.
"""
return None
@abstractmethod
def execute(self):
"""Perform this action.
Even though this is not the API, the action may raise
:class:`MAASAPIException` exceptions. When this happens, the view
will return to the client an http response reflecting the exception.
:return: A human-readable message confirming that the action has been
performed. It will be shown as an informational notice on the
Node page.
"""
def is_permitted(self):
"""Does the current user have the permission required?"""
return self.user.has_perm(self.permission, self.node)
# Uninitialized inhibititions cache.
_cached_inhibition = object()
@property
def inhibition(self):
"""Caching version of `inhibit`."""
if self._cached_inhibition == NodeAction._cached_inhibition:
self._cached_inhibition = self.inhibit()
return self._cached_inhibition
class Delete(NodeAction):
"""Delete a node."""
display = "Delete node"
actionable_statuses = ALL_STATUSES
permission = NODE_PERMISSION.ADMIN
def inhibit(self):
if self.node.status == NODE_STATUS.ALLOCATED:
return "You cannot delete this node because it's in use."
return None
def execute(self):
"""Redirect to the delete view's confirmation page.
The rest of deletion is handled by a specialized deletion view.
All that the action really does is get you to its are-you-sure
page.
"""
raise Redirect(reverse('node-delete', args=[self.node.system_id]))
class AcceptAndCommission(NodeAction):
"""Accept a node into the MAAS, and start the commissioning process."""
display = "Accept & commission"
actionable_statuses = (NODE_STATUS.DECLARED, )
permission = NODE_PERMISSION.ADMIN
def execute(self):
self.node.start_commissioning(self.user)
return "Node commissioning started."
class RetryCommissioning(NodeAction):
"""Retry commissioning of a node that failed previously."""
display = "Retry commissioning"
actionable_statuses = (NODE_STATUS.FAILED_TESTS, )
permission = NODE_PERMISSION.ADMIN
def execute(self):
self.node.start_commissioning(self.user)
return "Started a new attempt to commission this node."
class StartNode(NodeAction):
"""Acquire and start a node."""
display = "Start node"
actionable_statuses = (NODE_STATUS.READY, )
permission = NODE_PERMISSION.VIEW
def inhibit(self):
"""The user must have an SSH key, so that they access the node."""
if not SSHKey.objects.get_keys_for_user(self.user).exists():
return dedent("""\
You have no means of accessing the node after starting it.
Register an SSH key first. Do this on your Preferences
screen: click on the menu with your name at the top of the
page, select Preferences, and look for the "SSH keys" section.
""")
return None
def execute(self):
# The UI does not use OAuth, so there is no token to pass to the
# acquire() call.
self.node.acquire(self.user, token=None)
# Be sure to acquire before starting, or start_nodes will think
# the node ineligible based on its un-acquired status.
Node.objects.start_nodes([self.node.system_id], self.user)
return dedent("""\
This node is now allocated to you.
It has been asked to start up.
""")
ACTION_CLASSES = (
Delete,
AcceptAndCommission,
RetryCommissioning,
StartNode,
)
def compile_node_actions(node, user, request=None, classes=ACTION_CLASSES):
"""Provide :class:`NodeAction` objects for given request.
:param node: The :class:`Node` that the request pertains to.
:param user: The :class:`User` making the request.
:param request: The :class:`HttpRequest` being serviced. It may be used
to obtain information about the OAuth token being used.
:return: An :class:`OrderedDict` mapping applicable actions' display names
to corresponding :class:`NodeAction` instances. The dict is ordered
for consistent display.
"""
applicable_actions = (
action_class(node, user, request)
for action_class in classes
if node.status in action_class.actionable_statuses)
return OrderedDict(
(action.display, action)
for action in applicable_actions
if action.is_permitted())
|