launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06926
[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):