← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~gz/maas/node_search_view_jujuesque into lp:maas

 

Martin Packman has proposed merging lp:~gz/maas/node_search_view_jujuesque into lp:maas with lp:~gz/maas/node_search_view as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~gz/maas/node_search_view_jujuesque/+merge/128539

As discussed in the prerequisite merge proposal, this branch adapts node search to use the same constraints syntax as the juju command line. It's a little more verbose and fussy, but means copy/pasting between the two will work happily. Note that _parse_constraint now has to raise exceptions rather than just delegating to the constraint resolution to do that, but the view code is unchanged.

Branch also includes a few nice to haves, moves the confusingly duplicated mappings next to each other so it's clearer why both exist, and adds an exception class with a nicer error for when you inevitably tyop a constraint name.
-- 
https://code.launchpad.net/~gz/maas/node_search_view_jujuesque/+merge/128539
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~gz/maas/node_search_view_jujuesque into lp:maas.
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py	2012-10-08 11:46:58 +0000
+++ src/maasserver/api.py	2012-10-08 16:21:23 +0000
@@ -157,6 +157,7 @@
     NodeGroupInterface,
     Tag,
     )
+from maasserver.models.node import CONSTRAINTS_MAAS_MAP
 from maasserver.preseed import (
     compose_enlistment_preseed_url,
     compose_preseed_url,
@@ -681,17 +682,6 @@
         return ('nodes_handler', [])
 
 
-# Map the constraint as passed in the request to the name of the constraint in
-# the database. Many of them have the same name
-_constraints_map = {
-    'name': 'hostname',
-    'tags': 'tags',
-    'arch': 'architecture',
-    'cpu_count': 'cpu_count',
-    'mem': 'memory',
-    }
-
-
 def extract_constraints(request_params):
     """Extract a dict of node allocation constraints from http parameters.
 
@@ -701,9 +691,9 @@
     :rtype: :class:`dict`
     """
     constraints = {}
-    for request_name in _constraints_map:
+    for request_name in CONSTRAINTS_MAAS_MAP:
         if request_name in request_params:
-            db_name = _constraints_map[request_name]
+            db_name = CONSTRAINTS_MAAS_MAP[request_name]
             constraints[db_name] = request_params[request_name]
     return constraints
 

=== modified file 'src/maasserver/exceptions.py'
--- src/maasserver/exceptions.py	2012-09-26 16:34:58 +0000
+++ src/maasserver/exceptions.py	2012-10-08 16:21:23 +0000
@@ -98,6 +98,16 @@
         return s
 
 
+class NoSuchConstraint(InvalidConstraint):
+    """Node allocation constraint given does not exist."""
+
+    def __init__(self, constraint):
+        super(InvalidConstraint, self).__init__(constraint, None)
+
+    def __str__(self):
+        return "No such '%s' constraint" % self.args[:1]
+
+
 class Redirect(MAASAPIException):
     """Redirect.  The exception message is the target URL."""
     api_error = httplib.FOUND

=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py	2012-10-05 16:33:37 +0000
+++ src/maasserver/models/node.py	2012-10-08 16:21:23 +0000
@@ -11,6 +11,8 @@
 
 __metaclass__ = type
 __all__ = [
+    "CONSTRAINTS_JUJU_MAP",
+    "CONSTRAINTS_MAAS_MAP",
     "NODE_TRANSITIONS",
     "Node",
     "update_hardware_details",
@@ -77,6 +79,26 @@
     return 'node-%s' % uuid1()
 
 
+# Mapping of constraint names as used by juju to Node field names
+CONSTRAINTS_JUJU_MAP = {
+    'maas-name': 'hostname',
+    'maas-tags': 'tags',
+    'arch': 'architecture',
+    'cpu': 'cpu_count',
+    'mem': 'memory',
+    }
+
+
+# Mapping of constraint names as used by the maas api to Node field names
+CONSTRAINTS_MAAS_MAP = {
+    'name': 'hostname',
+    'tags': 'tags',
+    'arch': 'architecture',
+    'cpu_count': 'cpu_count',
+    'mem': 'memory',
+    }
+
+
 # Information about valid node status transitions.
 # The format is:
 # {

=== modified file 'src/maasserver/tests/test_exceptions.py'
--- src/maasserver/tests/test_exceptions.py	2012-09-26 16:34:58 +0000
+++ src/maasserver/tests/test_exceptions.py	2012-10-08 16:21:23 +0000
@@ -17,6 +17,7 @@
 from maasserver.exceptions import (
     InvalidConstraint,
     MAASAPIBadRequest,
+    NoSuchConstraint,
     Redirect,
     )
 from maasserver.testing import extract_redirect
@@ -57,3 +58,7 @@
         err = InvalidConstraint("limbs", "hundreds", context)
         self.assertEqual(str(err),
             "Invalid 'limbs' constraint 'hundreds': " + str(context))
+
+    def test_NoSuchConstraint_str(self):
+        err = NoSuchConstraint("hue")
+        self.assertEqual(str(err), "No such 'hue' constraint")

=== modified file 'src/maasserver/tests/test_views_nodes.py'
--- src/maasserver/tests/test_views_nodes.py	2012-10-08 16:21:23 +0000
+++ src/maasserver/tests/test_views_nodes.py	2012-10-08 16:21:23 +0000
@@ -25,7 +25,10 @@
     NODE_AFTER_COMMISSIONING_ACTION,
     NODE_STATUS,
     )
-from maasserver.exceptions import NoRabbit
+from maasserver.exceptions import (
+    InvalidConstraint,
+    NoRabbit,
+    )
 from maasserver.forms import NodeActionForm
 from maasserver.models import (
     MACAddress,
@@ -369,17 +372,18 @@
         self.assertIn(qs, query_value)
 
     def test_node_list_query_error_on_missing_tag(self):
-        response = self.client.get(reverse('node-list'), {"query": "missing"})
+        response = self.client.get(reverse('node-list'),
+            {"query": "maas-tags=missing"})
         error_string = fromstring(response.content).xpath(
             "string(//div[@id='nodes']//p[@class='form-errors'])")
         self.assertRegexpMatches(error_string, "Invalid .* No such tag")
 
     def test_node_list_query_error_on_unknown_constraint(self):
         response = self.client.get(reverse('node-list'),
-            {"query": "color:red"})
+            {"query": "color=red"})
         error_string = fromstring(response.content).xpath(
             "string(//div[@id='nodes']//p[@class='form-errors'])")
-        self.assertRegexpMatches(error_string, "Invalid .* 'color:red'")
+        self.assertRegexpMatches(error_string, "Invalid .* No such constraint")
 
     def test_node_list_query_selects_subset(self):
         tag = factory.make_tag("shiny")
@@ -390,7 +394,7 @@
         node2.tags = [tag]
         node3.tags = []
         response = self.client.get(reverse('node-list'),
-            {"query": "shiny cpu:2"})
+            {"query": "maas-tags=shiny cpu=2"})
         node2_link = reverse('node-view', args=[node2.system_id])
         document = fromstring(response.content)
         node_links = document.xpath("//div[@id='nodes']/table//a/@href")
@@ -595,59 +599,65 @@
         self.assertEqual({}, constraints)
 
     def test_tag_leading_whitespace(self):
-        constraints = nodes_views._parse_constraints("\ttag")
+        constraints = nodes_views._parse_constraints("\tmaas-tags=tag")
         self.assertEqual({"tags": "tag"}, constraints)
 
     def test_tag_trailing_whitespace(self):
-        constraints = nodes_views._parse_constraints("tag\r\n")
+        constraints = nodes_views._parse_constraints("maas-tags=tag\r\n")
         self.assertEqual({"tags": "tag"}, constraints)
 
     def test_tag_unicode(self):
-        constraints = nodes_views._parse_constraints("\xa7")
+        constraints = nodes_views._parse_constraints("maas-tags=\xa7")
         self.assertEqual({"tags": "\xa7"}, constraints)
 
+    def test_tag_no_value(self):
+        self.assertRaises(InvalidConstraint,
+            nodes_views._parse_constraints, "maas-tags")
+
     def test_cpu(self):
-        constraints = nodes_views._parse_constraints("cpu:1.0")
+        constraints = nodes_views._parse_constraints("cpu=1.0")
         self.assertEqual({"cpu_count": "1.0"}, constraints)
 
     def test_cpu_count(self):
-        constraints = nodes_views._parse_constraints("cpu_count:1")
-        self.assertEqual({"cpu_count": "1"}, constraints)
+        self.assertRaises(InvalidConstraint,
+            nodes_views._parse_constraints, "cpu_count=1.0")
 
     def test_mem(self):
-        constraints = nodes_views._parse_constraints("mem:4096.0")
+        constraints = nodes_views._parse_constraints("mem=4096.0")
         self.assertEqual({"memory": "4096.0"}, constraints)
 
     def test_memory(self):
-        constraints = nodes_views._parse_constraints("memory:4096")
-        self.assertEqual({"memory": "4096"}, constraints)
+        self.assertRaises(InvalidConstraint,
+            nodes_views._parse_constraints, "memory=4096.0")
 
     def test_arch(self):
-        constraints = nodes_views._parse_constraints("arch:armhf/highbank")
+        constraints = nodes_views._parse_constraints("arch=armhf/highbank")
         self.assertEqual({"architecture": "armhf/highbank"}, constraints)
 
+    def test_arch_empty(self):
+        constraints = nodes_views._parse_constraints("arch=")
+        self.assertEqual({}, constraints)
+
     def test_name(self):
-        constraints = nodes_views._parse_constraints("name:node")
+        constraints = nodes_views._parse_constraints("maas-name=node")
         self.assertEqual({"hostname": "node"}, constraints)
 
+    def test_name_any(self):
+        constraints = nodes_views._parse_constraints("maas-name=any")
+        self.assertEqual({}, constraints)
+
     def test_name_unicode(self):
-        constraints = nodes_views._parse_constraints("name:\xa7")
+        constraints = nodes_views._parse_constraints("maas-name=\xa7")
         self.assertEqual({"hostname": "\xa7"}, constraints)
 
     def test_unknown_constraint(self):
-        constraints = nodes_views._parse_constraints("custom:fancy")
-        self.assertEqual({"tags": "custom:fancy"}, constraints)
+        self.assertRaises(InvalidConstraint,
+            nodes_views._parse_constraints, "custom=fancy")
 
     def test_unknown_unicode_constraint(self):
-        constraints = nodes_views._parse_constraints("custom:\xa7")
-        self.assertEqual({"tags": "custom:\xa7"}, constraints)
+        self.assertRaises(InvalidConstraint,
+            nodes_views._parse_constraints, "custom=\xa7")
 
     def test_multiple_tags_and_cpu(self):
-        constraints = nodes_views._parse_constraints("a_tag cpu:2 b_tag")
-        self.assertEqual({"cpu_count": "2", "tags": "a_tag b_tag"},
-            constraints)
-
-    def test_tag_and_cpu_and_mem(self):
-        constraints = nodes_views._parse_constraints("mem:1024 cpu:2 tag")
-        self.assertEqual({"cpu_count": "2", "memory": "1024", "tags": "tag"},
-            constraints)
+        constraints = nodes_views._parse_constraints("maas-tags=a,b cpu=2")
+        self.assertEqual({"cpu_count": "2", "tags": "a,b"}, constraints)

=== modified file 'src/maasserver/views/nodes.py'
--- src/maasserver/views/nodes.py	2012-10-08 16:21:23 +0000
+++ src/maasserver/views/nodes.py	2012-10-08 16:21:23 +0000
@@ -57,6 +57,7 @@
     MACAddress,
     Node,
     )
+from maasserver.models.node import CONSTRAINTS_JUJU_MAP
 from maasserver.models.node_constraint_filter import constrain_nodes
 from maasserver.preseed import (
     get_enlist_preseed,
@@ -80,33 +81,22 @@
         return {}
 
 
-# Hey look, it's another mapping like the one in maasserver.api
-_non_tag_constraints = {
-    "cpu": "cpu_count",
-    "cpu_count": "cpu_count",
-    "mem": "memory",
-    "memory": "memory",
-    "arch": "architecture",
-    "name": "hostname",
-}
-
-
 def _parse_constraints(query_string):
     """Turn query string from user into constraints dict
 
-    Ideally this would be pretty much the same as the juju constraints, but
-    there are a few things that don't fit nicely across the two models.
+    This is basically the same as the juju constraints, but will differ
+    somewhat in error handling. For instance, juju might reject a negative
+    cpu constraint whereas this lets it through to return zero results.
     """
     constraints = {}
-    tags = []
     for word in query_string.strip().split():
-        parts = word.split(":", 1)
-        if len(parts) == 2 and parts[0] in _non_tag_constraints:
-            constraints[_non_tag_constraints[parts[0]]] = parts[1]
-        else:
-            tags.append(word)
-    if tags:
-        constraints["tags"] = " ".join(tags)
+        parts = word.split("=", 1)
+        if parts[0] not in CONSTRAINTS_JUJU_MAP:
+            raise NoSuchConstraint(parts[0])
+        if len(parts) != 2:
+            raise InvalidConstraint(parts[0], "", "No constraint value given")
+        if parts[1] and parts[1] != "any":
+            constraints[CONSTRAINTS_JUJU_MAP[parts[0]]] = parts[1]
     return constraints
 
 


Follow ups