Fixes for landscape bundle.
authorPete Vander Giessen <petevg@gmail.com>
Fri, 16 Dec 2016 19:41:04 +0000 (14:41 -0500)
committerPete Vander Giessen <petevg@gmail.com>
Fri, 16 Dec 2016 22:49:04 +0000 (17:49 -0500)
Parses constraints in some places where we weren't parsing them.

Normalizes keys in machine params.

Adds support for Placements on an lxd container *or* a machine.

Substitutes lxd containers for lxc containers.

juju/application.py
juju/constraints.py
juju/model.py
juju/placement.py
tests/unit/test_constraints.py
tests/unit/test_placement.py

index 69f412f..55b9d1e 100644 (file)
@@ -88,7 +88,7 @@ class Application(model.ModelEntity):
 
         result = await app_facade.AddUnits(
             application=self.name,
-            placement=[parse_placement(to)] if to else None,
+            placement=parse_placement(to) if to else None,
             num_units=count,
         )
 
index 43c048a..97529e4 100644 (file)
@@ -28,6 +28,9 @@ FACTORS = {
     "P": 1024 * 1024 * 1024
 }
 
+SNAKE1 = re.compile(r'(.)([A-Z][a-z]+)')
+SNAKE2 = re.compile('([a-z0-9])([A-Z])')
+
 def parse(constraints):
     """
     Constraints must be expressed as a string containing only spaces
@@ -37,6 +40,9 @@ def parse(constraints):
     if constraints is None:
         return None
 
+    if constraints == "":
+        return None
+
     if type(constraints) is dict:
         # Fowards compatibilty: already parsed
         return constraints
@@ -52,6 +58,11 @@ def normalize_key(key):
     key = key.strip()
 
     key = key.replace("-", "_")  # Our _client lib wants "_" in place of "-"
+
+    # Convert camelCase to snake_case
+    key = SNAKE1.sub(r'\1_\2', key)
+    key = SNAKE2.sub(r'\1_\2', key).lower()
+
     return key
 
 
@@ -68,4 +79,7 @@ def normalize_value(value):
         values = [normalize_value(v) for v in values]
         return values
 
+    if value.isdigit():
+        return int(value)
+
     return value
index 7897d42..4a8bc03 100644 (file)
@@ -14,7 +14,7 @@ from theblues import charmstore
 from .client import client
 from .client import watcher
 from .client import connection
-from .constraints import parse as parse_constraints
+from .constraints import parse as parse_constraints, normalize_key
 from .delta import get_entity_delta
 from .delta import get_entity_class
 from .exceptions import DeadEntityException
@@ -914,7 +914,7 @@ class Model(object):
                 channel=channel,
                 charm_url=entity_id,
                 config=config,
-                constraints=constraints,
+                constraints=parse_constraints(constraints),
                 endpoint_bindings=bind,
                 num_units=num_units,
                 placement=placement,
@@ -1364,7 +1364,7 @@ class BundleHandler(object):
                 expects.
 
             container_type: string holding the type of the container (for
-                instance ""lxc" or kvm"). It is not specified for top level
+                instance ""lxd" or kvm"). It is not specified for top level
                 machines.
 
             parent_id: string holding a placeholder pointing to another
@@ -1375,6 +1375,10 @@ class BundleHandler(object):
         """
         params = params or {}
 
+        # Normalize keys
+        params = {normalize_key(k): params[k] for k in params.keys()}
+
+        # Fix up values, as necessary.
         if 'parent_id' in params:
             params['parent_id'] = self.resolve(params['parent_id'])
 
@@ -1382,6 +1386,12 @@ class BundleHandler(object):
             params.get('constraints'))
         params['jobs'] = params.get('jobs', ['JobHostUnits'])
 
+        if params.get('container_type') == 'lxc':
+            log.warning('Juju 2.0 does not support lxc containers. '
+                        'Converting containers to lxd.')
+            params['container_type'] = 'lxd'
+
+        # Submit the request.
         params = client.AddMachineParams(**params)
         results = await self.client_facade.AddMachines([params])
         error = results.machines[0].error
@@ -1450,7 +1460,7 @@ class BundleHandler(object):
             series=series,
             application=application,
             config=options,
-            constraints=constraints,
+            constraints=parse_constraints(constraints),
             storage=storage,
             endpoint_bindings=endpoint_bindings,
             resources=resources,
index 6f91aed..561bc40 100644 (file)
@@ -25,18 +25,28 @@ def parse(directive):
     if type(directive) in [dict, client.Placement]:
         # We've been handed something that we can simply hand back to
         # the api. (Forwards compatibility)
-        return directive
+        return [directive]
+
+    # Juju 2.0 can't handle lxc containers.
+    directive = directive.replace('lxc', 'lxd')
 
     if ":" in directive:
         # Planner has given us a scope and directive in string form
         scope, directive = directive.split(":")
-        return client.Placement(scope=scope, directive=directive)
+        return [client.Placement(scope=scope, directive=directive)]
 
     if directive.isdigit():
         # Planner has given us a machine id (we rely on juju core to
         # verify its validity.)
-        return client.Placement(scope=MACHINE_SCOPE, directive=directive)
+        return [client.Placement(scope=MACHINE_SCOPE, directive=directive)]
+
+    if "/" in directive:
+        machine, container, container_num = directive.split("/")
+        return [
+            client.Placement(scope=MACHINE_SCOPE, directive=machine),
+            client.Placement(scope=container, directive=container_num)
+        ]
 
     # Planner has probably given us a container type. Leave it up to
     # juju core to verify that it is valid.
-    return client.Placement(scope=directive)
+    return [client.Placement(scope=directive)]
index ec50bdd..cb9d773 100644 (file)
@@ -22,13 +22,15 @@ class TestConstraints(unittest.TestCase):
         self.assertEqual(_("test-key"), "test_key")
         self.assertEqual(_("test-key  "), "test_key")
         self.assertEqual(_("  test-key"), "test_key")
+        self.assertEqual(_("TestKey"), "test_key")
+        self.assertEqual(_("testKey"), "test_key")
 
     def test_normalize_val(self):
         _ = constraints.normalize_value
 
         self.assertEqual(_("10G"), 10 * 1024)
         self.assertEqual(_("10M"), 10)
-        self.assertEqual(_("10"), "10")
+        self.assertEqual(_("10"), 10)
         self.assertEqual(_("foo,bar"), ["foo", "bar"])
 
     def test_parse_constraints(self):
index 793266d..a78a28d 100644 (file)
@@ -11,10 +11,10 @@ class TestPlacement(unittest.TestCase):
 
     def test_parse_both_specified(self):
         res = placement.parse("foo:bar")
-        self.assertEqual(res.scope, "foo")
-        self.assertEqual(res.directive, "bar")
+        self.assertEqual(res[0].scope, "foo")
+        self.assertEqual(res[0].directive, "bar")
 
     def test_parse_machine(self):
         res = placement.parse("22")
-        self.assertEqual(res.scope, "#")
-        self.assertEqual(res.directive, "22")
+        self.assertEqual(res[0].scope, "#")
+        self.assertEqual(res[0].directive, "22")