← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/maas-status-refactor into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/maas-status-refactor into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/maas-status-refactor/+merge/99961

This branch adds status transition management in Node.  I've filled NODE_TRANSITIONS with the most sensible values… I'm not sure how to that that structure in a clever way (i.e. in a way that would not imply to simply duplicate the structure in the tests).

The end game is to change NODE_TRANSITIONS to be able to get the list of all the possible actions on a Node (to exploit that in the UI and generate the appropriate buttons/handlers accordingly).
-- 
https://code.launchpad.net/~rvb/maas/maas-status-refactor/+merge/99961
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-status-refactor into lp:maas.
=== modified file 'src/maasserver/models.py'
--- src/maasserver/models.py	2012-03-29 11:40:39 +0000
+++ src/maasserver/models.py	2012-03-29 15:19:19 +0000
@@ -13,6 +13,7 @@
     "create_auth_token",
     "generate_node_system_id",
     "get_auth_tokens",
+    "get_db_state",
     "get_html_display_for_key",
     "Config",
     "FileStorage",
@@ -148,6 +149,45 @@
 NODE_STATUS_CHOICES_DICT = OrderedDict(NODE_STATUS_CHOICES)
 
 
+NODE_TRANSITIONS = {
+    None: [
+        NODE_STATUS.DECLARED,  # Authenticated enlistment.
+        NODE_STATUS.READY,  # Anonymous enlistement.
+        NODE_STATUS.MISSING,
+        NODE_STATUS.RETIRED,
+        ],
+    NODE_STATUS.DECLARED: [
+        NODE_STATUS.COMMISSIONING,
+        NODE_STATUS.MISSING,
+        NODE_STATUS.RETIRED,
+        ],
+    NODE_STATUS.COMMISSIONING: [
+        NODE_STATUS.FAILED_TESTS,
+        NODE_STATUS.READY,
+        NODE_STATUS.RETIRED,
+        NODE_STATUS.MISSING,
+        ],
+    NODE_STATUS.READY: [
+        NODE_STATUS.ALLOCATED,
+        NODE_STATUS.RESERVED,
+        NODE_STATUS.RETIRED,
+        NODE_STATUS.MISSING,
+        ],
+    NODE_STATUS.ALLOCATED: [
+        NODE_STATUS.READY,
+        NODE_STATUS.RETIRED,
+        NODE_STATUS.MISSING,
+        ],
+    NODE_STATUS.MISSING: [
+        NODE_STATUS.DECLARED,
+        NODE_STATUS.READY,
+        ],
+    NODE_STATUS.RETIRED: [
+        NODE_STATUS.MISSING,
+        ],
+    }
+
+
 class NODE_AFTER_COMMISSIONING_ACTION:
     """The vocabulary of a `Node`'s possible value for its field
     after_commissioning_action.
@@ -354,7 +394,7 @@
         :param user_data: Optional blob of user-data to be made available to
             the nodes through the metadata service.  If not given, any
             previous user data is used.
-        :type user_data: str
+        :type user_data: basestring
         :return: Those Nodes for which power-on was actually requested.
         :rtype: list
         """
@@ -367,6 +407,21 @@
         return nodes
 
 
+def get_db_state(instance, field_name):
+    """Get the persisted state of the given field for the given instance.
+
+    :param instance: The model instance to consider.
+    :type instance: :class:`django.db.models.Model`
+    :param field_name: The name of the field to return.
+    :type field_name: basestring
+    """
+    try:
+        return getattr(
+            instance.__class__.objects.get(pk=instance.pk), field_name)
+    except instance.DoesNotExist:
+        return None
+
+
 class Node(CommonInfo):
     """A `Node` represents a physical machine used by the MAAS Server.
 
@@ -424,6 +479,17 @@
         else:
             return self.system_id
 
+    def clean(self, *args, **kwargs):
+        super(Node, self).clean(*args, **kwargs)
+        # Check that the status transition (if any) is valid.
+        old_status = get_db_state(self, 'status')
+        if self.status != old_status:
+            if self.status not in NODE_TRANSITIONS[old_status]:
+                raise ValidationError(
+                    {'status': "Invalid transition: %s -> %s" % (
+                        NODE_STATUS_CHOICES_DICT[old_status],
+                        NODE_STATUS_CHOICES_DICT[self.status])})
+
     def display_status(self):
         """Return status text as displayed to the user.
 

=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py	2012-03-28 16:00:26 +0000
+++ src/maasserver/testing/factory.py	2012-03-29 15:19:19 +0000
@@ -41,7 +41,7 @@
         """
         return random.choice(list(map_enum(enum).values()))
 
-    def getRandomChoice(self, choices):
+    def getRandomChoice(self, choices, but_not=None):
         """Pick a random item from `choices`.
 
         :param choices: A sequence of choices in Django form choices format:
@@ -49,9 +49,14 @@
                 ('choice_id_1', "Choice name 1"),
                 ('choice_id_2', "Choice name 2"),
             ]
+        :param but_not: A list of choices'ids to exclude.
+        :type but_not: Sequence.
         :return: The "id" portion of a random choice out of `choices`.
         """
-        return random.choice(choices)[0]
+        if but_not is None:
+            but_not = []
+        return random.choice(
+            [choice for choice in choices if choice[0] not in but_not])[0]
 
     def make_node(self, hostname='', set_hostname=False, status=None,
                   architecture=ARCHITECTURE.i386, **kwargs):

=== modified file 'src/maasserver/testing/tests/test_factory.py'
--- src/maasserver/testing/tests/test_factory.py	2012-03-15 16:51:15 +0000
+++ src/maasserver/testing/tests/test_factory.py	2012-03-29 15:19:19 +0000
@@ -34,3 +34,9 @@
         self.assertIn(
             factory.getRandomChoice(options),
             [option[0] for option in options])
+
+    def test_getRandomChoice_can_exclude_choices(self):
+        options = [(2, 'b'), (10, 'j')]
+        but_not = [2]
+        self.assertEqual(
+            10, factory.getRandomChoice(options, but_not=but_not))

=== modified file 'src/maasserver/tests/test_models.py'
--- src/maasserver/tests/test_models.py	2012-03-29 13:19:40 +0000
+++ src/maasserver/tests/test_models.py	2012-03-29 15:19:19 +0000
@@ -34,12 +34,14 @@
     FileStorage,
     GENERIC_CONSUMER,
     get_auth_tokens,
+    get_db_state,
     get_default_config,
     get_html_display_for_key,
     HELLIPSIS,
     MACAddress,
     Node,
     NODE_STATUS,
+    NODE_STATUS_CHOICES,
     NODE_STATUS_CHOICES_DICT,
     SSHKey,
     SYSTEM_USERS,
@@ -188,6 +190,35 @@
         node.release()
         self.assertEqual((NODE_STATUS.READY, None), (node.status, node.owner))
 
+    def test_full_clean_checks_status_transition_and_raises_if_invalid(self):
+        # RETIRED -> READY is an invalid transition.
+        node = factory.make_node(
+            status=NODE_STATUS.RETIRED, owner=factory.make_user())
+        node.status = NODE_STATUS.READY
+        self.assertRaisesRegexp(
+            ValidationError,
+            str({'status': u'Invalid transition: Retired -> Ready'}),
+            node.full_clean)
+
+    def test_full_clean_passes_if_status_unchanged(self):
+        status = factory.getRandomChoice(NODE_STATUS_CHOICES)
+        node = factory.make_node(status=status)
+        node.status = status
+        node.full_clean()
+        # No ValidationError.
+
+
+class GetDbStateTest(TestCase):
+    """Testing for the method `get_db_state`."""
+
+    def test_get_db_state_returns_db_state(self):
+        status = factory.getRandomChoice(NODE_STATUS_CHOICES)
+        node = factory.make_node(status=status)
+        another_status = factory.getRandomChoice(
+            NODE_STATUS_CHOICES, but_not=[status])
+        node.status = another_status
+        self.assertEqual(status, get_db_state(node, 'status'))
+
 
 class NodeManagerTest(TestCase):