← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/maas-admin-approve-nodes into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/maas-admin-approve-nodes into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/maas-admin-approve-nodes/+merge/100175

This branch features the backend side of "An admin can enlist nodes" story.  I've add NODE_TRANSITIONS_METHODS which features transition information that associates a status change to a method being called on the node.  The goal is to add actions to the UI only by modifying NODE_TRANSITIONS_METHODS.

I've added tests to make sure that the structure of both NODE_TRANSITIONS_METHODS and NODE_TRANSITIONS are correct.  We will rely on these structures in the follow-up branch (https://code.launchpad.net/~rvb/maas/maas-admin-approve-nodes-ui/+merge/100176). Also, don't be surprised by the new permission check that returns always false: Django replies always true when a permission check is done on a super user so we only have to take care of the case where the user is not a super user.
-- 
https://code.launchpad.net/~rvb/maas/maas-admin-approve-nodes/+merge/100175
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-admin-approve-nodes into lp:maas.
=== modified file 'src/maasserver/models.py'
--- src/maasserver/models.py	2012-03-30 17:01:12 +0000
+++ src/maasserver/models.py	2012-04-01 19:34:22 +0000
@@ -18,6 +18,7 @@
     "Config",
     "FileStorage",
     "NODE_STATUS",
+    "NODE_TRANSITIONS",
     "Node",
     "MACAddress",
     "SSHKey",
@@ -151,6 +152,17 @@
 NODE_STATUS_CHOICES_DICT = OrderedDict(NODE_STATUS_CHOICES)
 
 
+# Information about valid node status transitions.
+# The format is:
+# {
+#  old_status1: [
+#      new_status11,
+#      new_status12,
+#      new_status13,
+#      ],
+# ...
+# }
+#
 NODE_TRANSITIONS = {
     None: [
         NODE_STATUS.DECLARED,
@@ -537,7 +549,7 @@
         """Add a new MAC Address to this `Node`.
 
         :param mac_address: The MAC Address to be added.
-        :type mac_address: str
+        :type mac_address: basestring
         :raises: django.core.exceptions.ValidationError_
 
         .. _django.core.exceptions.ValidationError: https://
@@ -1185,6 +1197,9 @@
             return obj.owner in (None, user)
         elif perm == 'edit':
             return obj.owner == user
+        elif perm == 'admin':
+            # 'admin' permission is solely granted to superusers.
+            return False
         else:
             raise NotImplementedError(
                 'Invalid permission check (invalid permission name).')

=== modified file 'src/maasserver/tests/test_auth.py'
--- src/maasserver/tests/test_auth.py	2012-03-22 11:35:03 +0000
+++ src/maasserver/tests/test_auth.py	2012-04-01 19:34:22 +0000
@@ -124,6 +124,11 @@
         self.assertTrue(backend.has_perm(
             user, 'edit', make_allocated_node(owner=user)))
 
+    def test_user_has_not_admin_permission_on_node(self):
+        backend = MAASAuthorizationBackend()
+        user = factory.make_user()
+        self.assertFalse(backend.has_perm(user, 'admin', factory.make_node()))
+
 
 class TestNodeVisibility(TestCase):
 

=== modified file 'src/maasserver/tests/test_models.py'
--- src/maasserver/tests/test_models.py	2012-03-30 17:01:12 +0000
+++ src/maasserver/tests/test_models.py	2012-04-01 19:34:22 +0000
@@ -44,6 +44,7 @@
     NODE_STATUS,
     NODE_STATUS_CHOICES,
     NODE_STATUS_CHOICES_DICT,
+    NODE_TRANSITIONS,
     SSHKey,
     SYSTEM_USERS,
     UserProfile,
@@ -303,6 +304,21 @@
         pass
 
 
+class NodeTransitionsTests(TestCase):
+    """Test the structure of NODE_TRANSITIONS."""
+
+    def test_NODE_TRANSITIONS_initial_states(self):
+        for initial_state in NODE_TRANSITIONS:
+            self.assertIn(
+                initial_state, NODE_STATUS_CHOICES_DICT.keys() + [None])
+
+    def test_NODE_TRANSITIONS_destination_state(self):
+        for transitions in NODE_TRANSITIONS.values():
+            for destination_state in transitions:
+                self.assertIn(
+                    destination_state, NODE_STATUS_CHOICES_DICT.keys())
+
+
 class GetDbStateTest(TestCase):
     """Testing for the method `get_db_state`."""
 

=== modified file 'src/maasserver/tests/test_views.py'
--- src/maasserver/tests/test_views.py	2012-03-29 11:40:39 +0000
+++ src/maasserver/tests/test_views.py	2012-04-01 19:34:22 +0000
@@ -30,6 +30,8 @@
 from maasserver.models import (
     Config,
     NODE_AFTER_COMMISSIONING_ACTION,
+    NODE_STATUS,
+    NODE_STATUS_CHOICES_DICT,
     POWER_TYPE_CHOICES,
     SSHKey,
     UserProfile,
@@ -48,8 +50,10 @@
     make_path_relative,
     )
 from maasserver.views import (
+    available_transition_methods,
     get_longpoll_context,
     get_yui_location,
+    NODE_TRANSITIONS_METHODS,
     proxy_to_longpoll,
     )
 from maastesting.rabbit import uses_rabbit_fixture
@@ -240,6 +244,50 @@
         self.assertEqual("Invalid file type requested.", response.content)
 
 
+class NodeTransitionsMethodsTests(TestCase):
+    """Test the structure of NODE_TRANSITIONS_METHODS."""
+
+    def test_NODE_TRANSITION_METHODS_initial_states(self):
+        for state in NODE_TRANSITIONS_METHODS:
+            self.assertIn(
+                state, NODE_STATUS_CHOICES_DICT.keys() + [None])
+
+    def test_NODE_TRANSITIONS_METHODS_transitions(self):
+        for transitions in NODE_TRANSITIONS_METHODS.values():
+            for transition in transitions:
+                self.assertItemsEqual(
+                    ['display', 'name', 'permission'], transition.keys())
+                # The 'permission' is a valid Node permission.
+                self.assertIn(
+                    transition['permission'], ['edit', 'view', 'admin'])
+                # The 'name' is a basestring.
+                self.assertIsInstance(transition['name'], basestring)
+                # The 'display' is a basestring.
+                self.assertIsInstance(transition['display'], basestring)
+
+
+class AvailableTransitionsMethodsTests(TestCase):
+
+    def test_available_transition_methods_for_declared_node_admin(self):
+        # An admin has access to the "Enlist node" transition for a
+        # 'Declared' node.
+        admin = factory.make_admin()
+        node = factory.make_node(status=NODE_STATUS.DECLARED)
+        self.assertItemsEqual(
+            [{
+                'display': 'Enlist node',
+                'name': 'accept_enlistment_action',
+                'permission': 'admin',
+            }],
+            available_transition_methods(node, admin))
+
+    def test_available_transition_methods_for_declared_node_simple_user(self):
+        # A simple user sees not transition for a 'Declared' node.
+        user = factory.make_user()
+        node = factory.make_node(status=NODE_STATUS.DECLARED)
+        self.assertItemsEqual([], available_transition_methods(node, user))
+
+
 class TestUtilities(TestCase):
 
     def test_get_yui_location_if_static_root_is_none(self):

=== modified file 'src/maasserver/views.py'
--- src/maasserver/views.py	2012-03-28 16:13:04 +0000
+++ src/maasserver/views.py	2012-04-01 19:34:22 +0000
@@ -14,9 +14,11 @@
     "AccountsDelete",
     "AccountsEdit",
     "AccountsView",
+    "available_transition_methods",
     "combo_view",
     "login",
     "logout",
+    "NODE_TRANSITIONS_METHODS",
     "NodeListView",
     "NodesCreateView",
     "NodeView",
@@ -83,10 +85,58 @@
 from maasserver.messages import messaging
 from maasserver.models import (
     Node,
+    NODE_STATUS,
     SSHKey,
     UserProfile,
     )
 
+# Node transitions methods.
+# The format is:
+# {
+#     old_status1: [
+#         {
+#             'display': display_string11,  # The name of the transition
+#                                           # (to be displayed in the UI).
+#             'name': transition_name11,  # The name of the transition.
+#             'permission': permission_required11,
+#         },
+#     ]
+# ...
+#
+NODE_TRANSITIONS_METHODS = {
+    NODE_STATUS.DECLARED: [
+        {
+            'display': "Enlist node",
+            'name': "accept_enlistment_action",
+            'permission': 'admin'
+        },
+    ],
+}
+
+
+def available_transition_methods(node, user):
+    """Return the transitions that this user is allowed to perform on
+    a node.
+
+    :param node: The node for which the check should be performed.
+    :type node: :class:`maasserver.models.Node`
+    :param user: The user used to perform the permission checks.  Only the
+        transitions available to this user will be returned.
+    :type user: :class:`django.contrib.auth.models.User`
+    :return: A list of transition dicts (each dict contains 3 values:
+        'name': the name of the transition, 'permission': the permission
+        required to perform this transition, 'method': the name of the
+        method to execute on the node to perform the transition).
+    :rtype: Sequence
+    """
+    valid_transitions = []
+    node_transitions = NODE_TRANSITIONS_METHODS.get(node.status, ())
+    for node_transition in node_transitions:
+        if user.has_perm(node_transition['permission'], node):
+            # The user can perform the transition.
+            valid_transitions.append(node_transition)
+    return valid_transitions
+
 
 def login(request):
     extra_context = {