← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/maas/cobbler-edit-before-add into lp:maas

 

Gavin Panella has proposed merging lp:~allenap/maas/cobbler-edit-before-add into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/maas/cobbler-edit-before-add/+merge/97038
-- 
https://code.launchpad.net/~allenap/maas/cobbler-edit-before-add/+merge/97038
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/maas/cobbler-edit-before-add into lp:maas.
=== modified file 'Makefile'
--- Makefile	2012-03-12 13:50:40 +0000
+++ Makefile	2012-03-12 15:09:00 +0000
@@ -44,8 +44,8 @@
 	utilities/maasdb start ./db/ disposable
 
 test: bin/test.maas bin/test.pserv
-	bin/test.maas
-	bin/test.pserv
+	bin/test.maas -v
+	bin/test.pserv -v
 
 lint: sources = setup.py src templates utilities
 lint: bin/flake8

=== modified file 'src/provisioningserver/cobblerclient.py'
--- src/provisioningserver/cobblerclient.py	2012-03-09 05:53:22 +0000
+++ src/provisioningserver/cobblerclient.py	2012-03-12 15:09:00 +0000
@@ -96,6 +96,15 @@
         return "invalid token: " in exception.faultString
 
 
+def looks_like_object_not_found(exception):
+    """Does `exception` look like an unknown object failure?"""
+    if not hasattr(exception, 'faultString'):
+        # An unknown object failure would come as an xmlrpclib.Fault.
+        return False
+    else:
+        return "internal error, unknown " in exception.faultString
+
+
 class CobblerSession:
     """A session on the Cobbler XMLRPC API.
 
@@ -486,14 +495,24 @@
             (cls._normalize_attribute(key), value)
             for key, value in attributes.items())
 
-        # Overwrite any existing object of the same name.  Unfortunately
-        # this parameter goes into the "attributes," and seems to be
-        # stored along with them.  Its value doesn't matter.
-        args.setdefault('clobber', True)
-
-        success = yield session.call(
-            'xapi_object_edit', cls.object_type, name, 'add', args,
-            session.token_placeholder)
+        # Do not clobber under any circumstances.
+        if "clobber" in args:
+            del args["clobber"]
+
+        try:
+            # Attempt to edit an existing object.
+            success = yield session.call(
+                'xapi_object_edit', cls.object_type, name, 'edit', args,
+                session.token_placeholder)
+        except xmlrpclib.Fault as e:
+            # If it was not found, add the object.
+            if looks_like_object_not_found(e):
+                success = yield session.call(
+                    'xapi_object_edit', cls.object_type, name, 'add', args,
+                    session.token_placeholder)
+            else:
+                raise
+
         if not success:
             raise RuntimeError(
                 "Cobbler refused to create %s '%s'.  Attributes: %s"

=== modified file 'src/provisioningserver/testing/fakecobbler.py'
--- src/provisioningserver/testing/fakecobbler.py	2012-03-09 05:56:28 +0000
+++ src/provisioningserver/testing/fakecobbler.py	2012-03-12 15:09:00 +0000
@@ -11,9 +11,10 @@
 __metaclass__ = type
 __all__ = [
     'fake_auth_failure_string',
+    'fake_object_not_found_string',
+    'fake_token',
     'FakeCobbler',
     'FakeTwistedProxy',
-    'fake_token',
     'make_fake_cobbler_session',
     ]
 
@@ -41,6 +42,13 @@
     return "<class 'cobbler.exceptions.CX'>:'invalid token: %s'" % token
 
 
+def fake_object_not_found_string(object_type, object_name):
+    """Fake a Cobbler unknown object fault string."""
+    return (
+        "<class 'cobbler.cexceptions.CX'>:'internal error, "
+        "unknown %s name %s'" % (object_type, object_name))
+
+
 def fake_token(user=None, custom_id=None):
     """Make up a fake auth token.
 
@@ -211,7 +219,7 @@
         if handle is None:
             handle = self._get_handle_if_present(None, object_type, name)
         if handle is None:
-            raise Fault(1, "Unknown %s: %s." % (object_type, name))
+            raise Fault(1, fake_object_not_found_string(object_type, name))
         return handle
 
     def _api_find_objects(self, object_type, criteria):

=== modified file 'src/provisioningserver/tests/test_api.py'
--- src/provisioningserver/tests/test_api.py	2012-03-09 11:57:46 +0000
+++ src/provisioningserver/tests/test_api.py	2012-03-12 15:09:00 +0000
@@ -266,17 +266,20 @@
         return d
 
     @inlineCallbacks
-    def add_distro(self, papi):
+    def add_distro(self, papi, name=None):
         """Creates a new distro object via `papi`.
 
-        Arranges for it to be deleted during test clean-up.
+        Arranges for it to be deleted during test clean-up. If `name` is not
+        specified, `fake_name` will be called to obtain one.
         """
+        if name is None:
+            name = fake_name()
         # For the initrd and kernel, use a file that we know will exist for a
         # running Cobbler instance (at least, on Ubuntu) so that we can test
         # against remote instances, like one in odev.
         initrd = "/etc/cobbler/settings"
         kernel = "/etc/cobbler/version"
-        distro_name = yield papi.add_distro(fake_name(), initrd, kernel)
+        distro_name = yield papi.add_distro(name, initrd, kernel)
         self.addCleanup(
             self.cleanup_objects,
             papi.delete_distros_by_name,
@@ -284,15 +287,18 @@
         returnValue(distro_name)
 
     @inlineCallbacks
-    def add_profile(self, papi, distro_name=None):
+    def add_profile(self, papi, name=None, distro_name=None):
         """Creates a new profile object via `papi`.
 
-        Arranges for it to be deleted during test clean-up. If `distro_name`
+        Arranges for it to be deleted during test clean-up. If `name` is not
+        specified, `fake_name` will be called to obtain one. If `distro_name`
         is not specified, one will be obtained by calling `add_distro`.
         """
+        if name is None:
+            name = fake_name()
         if distro_name is None:
             distro_name = yield self.add_distro(papi)
-        profile_name = yield papi.add_profile(fake_name(), distro_name)
+        profile_name = yield papi.add_profile(name, distro_name)
         self.addCleanup(
             self.cleanup_objects,
             papi.delete_profiles_by_name,
@@ -300,20 +306,23 @@
         returnValue(profile_name)
 
     @inlineCallbacks
-    def add_node(self, papi, profile_name=None, metadata=None):
+    def add_node(self, papi, name=None, profile_name=None, metadata=None):
         """Creates a new node object via `papi`.
 
-        Arranges for it to be deleted during test clean-up. If `profile_name`
+        Arranges for it to be deleted during test clean-up. If `name` is not
+        specified, `fake_name` will be called to obtain one. If `profile_name`
         is not specified, one will be obtained by calling `add_profile`. If
         `metadata` is not specified, it will be obtained by calling
         `fake_node_metadata`.
         """
+        if name is None:
+            name = fake_name()
         if profile_name is None:
             profile_name = yield self.add_profile(papi)
         if metadata is None:
             metadata = fake_node_metadata()
         node_name = yield papi.add_node(
-            fake_name(), profile_name, metadata)
+            name, profile_name, metadata)
         self.addCleanup(
             self.cleanup_objects,
             papi.delete_nodes_by_name,
@@ -349,6 +358,23 @@
         self.assertItemsEqual([node_name], nodes)
 
     @inlineCallbacks
+    def _test_add_object_twice(self, method):
+        # Adding an object twice is allowed.
+        papi = self.get_provisioning_api()
+        object_name1 = yield method(papi)
+        object_name2 = yield method(papi, object_name1)
+        self.assertEqual(object_name1, object_name2)
+
+    def test_add_distro_twice(self):
+        return self._test_add_object_twice(self.add_distro)
+
+    def test_add_profile_twice(self):
+        return self._test_add_object_twice(self.add_profile)
+
+    def test_add_node_twice(self):
+        return self._test_add_object_twice(self.add_node)
+
+    @inlineCallbacks
     def test_modify_distros(self):
         papi = self.get_provisioning_api()
         distro_name = yield self.add_distro(papi)
@@ -371,7 +397,7 @@
         papi = self.get_provisioning_api()
         distro1_name = yield self.add_distro(papi)
         distro2_name = yield self.add_distro(papi)
-        profile_name = yield self.add_profile(papi, distro1_name)
+        profile_name = yield self.add_profile(papi, None, distro1_name)
         yield papi.modify_profiles({profile_name: {"distro": distro2_name}})
         values = yield papi.get_profiles_by_name([profile_name])
         self.assertEqual(distro2_name, values[profile_name]["distro"])
@@ -380,9 +406,9 @@
     def test_modify_nodes(self):
         papi = self.get_provisioning_api()
         distro_name = yield self.add_distro(papi)
-        profile1_name = yield self.add_profile(papi, distro_name)
-        profile2_name = yield self.add_profile(papi, distro_name)
-        node_name = yield self.add_node(papi, profile1_name)
+        profile1_name = yield self.add_profile(papi, None, distro_name)
+        profile2_name = yield self.add_profile(papi, None, distro_name)
+        node_name = yield self.add_node(papi, None, profile1_name)
         yield papi.modify_nodes({node_name: {"profile": profile2_name}})
         values = yield papi.get_nodes_by_name([node_name])
         self.assertEqual(profile2_name, values[node_name]["profile"])
@@ -468,7 +494,7 @@
         # Create some profiles via the Provisioning API.
         profiles_expected = set()
         for num in range(3):
-            profile_name = yield self.add_profile(papi, distro_name)
+            profile_name = yield self.add_profile(papi, None, distro_name)
             profiles_expected.add(profile_name)
         profiles_after = yield papi.get_profiles()
         profiles_created = set(profiles_after) - set(profiles_before)
@@ -480,7 +506,7 @@
         profile_name = yield self.add_profile(papi)
         node_names = set()
         for num in range(3):
-            node_name = yield self.add_node(papi, profile_name)
+            node_name = yield self.add_node(papi, None, profile_name)
             node_names.add(node_name)
         nodes = yield papi.get_nodes()
         self.assertSetEqual(node_names, node_names.intersection(nodes))

=== modified file 'src/provisioningserver/tests/test_cobblersession.py'
--- src/provisioningserver/tests/test_cobblersession.py	2012-03-09 06:17:46 +0000
+++ src/provisioningserver/tests/test_cobblersession.py	2012-03-12 15:09:00 +0000
@@ -18,6 +18,7 @@
 from provisioningserver import cobblerclient
 from provisioningserver.testing.fakecobbler import (
     fake_auth_failure_string,
+    fake_object_not_found_string,
     fake_token,
     )
 from testtools.content import text_content
@@ -254,6 +255,20 @@
         self.assertTrue(
             cobblerclient.looks_like_auth_expiry(make_auth_failure()))
 
+    def test_looks_like_object_not_found_for_regular_exception(self):
+        self.assertFalse(
+            cobblerclient.looks_like_object_not_found(RuntimeError("Error")))
+
+    def test_looks_like_object_not_found_for_other_Fault(self):
+        self.assertFalse(
+            cobblerclient.looks_like_object_not_found(
+                Fault(1, "Missing sprocket")))
+
+    def test_looks_like_object_not_found_recognizes_object_not_found(self):
+        error = Fault(1, fake_object_not_found_string("distro", "bob"))
+        self.assertTrue(
+            cobblerclient.looks_like_object_not_found(error))
+
     @inlineCallbacks
     def test_call_reauthenticates_and_retries_on_auth_failure(self):
         # If a call triggers an authentication error, call()
@@ -416,6 +431,29 @@
                 session, 'incomplete_system', {})
 
     @inlineCallbacks
+    def test_new_attempts_edit_before_creating_new(self):
+        # CobblerObject.new always attempts an extended edit operation on the
+        # given object first, following by an add should the object not yet
+        # exist.
+        session = make_recording_session()
+        not_found_string = fake_object_not_found_string("system", "carcass")
+        not_found = Fault(1, not_found_string)
+        session.proxy.set_return_values([not_found, True])
+        yield cobblerclient.CobblerSystem.new(
+            session, "carcass", {"profile": "heartwork"})
+        expected_calls = [
+            # First an edit is attempted...
+            ("xapi_object_edit", "system", "carcass", "edit",
+             {"name": "carcass", "profile": "heartwork"},
+             session.token),
+            # Followed by an add.
+            ("xapi_object_edit", "system", "carcass", "add",
+             {"name": "carcass", "profile": "heartwork"},
+             session.token),
+            ]
+        self.assertEqual(expected_calls, session.proxy.calls)
+
+    @inlineCallbacks
     def test_modify(self):
         session = make_recording_session()
         session.proxy.set_return_values([True])
@@ -447,7 +485,6 @@
         # Fake that Cobbler holds the following attributes about the distro
         # just created.
         values_stored = {
-            "clobber": True,
             "initrd": "an_initrd",
             "kernel": "a_kernel",
             "likes": "cabbage",
@@ -472,8 +509,7 @@
         # Fake that Cobbler holds the following attributes about the distros
         # just created.
         values_stored = [
-            {"clobber": True,
-             "initrd": "an_initrd",
+            {"initrd": "an_initrd",
              "kernel": "a_kernel",
              "likes": "cabbage",
              "name": "alice"},