← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/maas/cluster-params into lp:maas

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/cluster-params into lp:maas with lp:~jtv/maas/list-networks as a prerequisite.

Commit message:
In start_cluster_controller: require a uuid argument, discover network interfaces, and pass both to the "register" API call which needs to know both items.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jtv/maas/cluster-params/+merge/126520

See commit message.  As discussed with Julian.  There is no need to pass a nodegroup name to the API, nor DHCP ranges and broadcast addresses; both can be set in the UI when appropriate.


Jeroen
-- 
https://code.launchpad.net/~jtv/maas/cluster-params/+merge/126520
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/maas/cluster-params into lp:maas.
=== modified file 'src/provisioningserver/network.py'
--- src/provisioningserver/network.py	2012-09-26 17:47:23 +0000
+++ src/provisioningserver/network.py	2012-09-26 17:47:23 +0000
@@ -18,9 +18,8 @@
     'discover_networks',
     ]
 
-from io import BytesIO
 import os
-from subprocess import check_call
+from subprocess import check_output
 
 
 class InterfaceInfo:
@@ -29,34 +28,37 @@
     def __init__(self, interface):
         self.interface = interface
         self.ip = None
-        self.mask = None
+        self.subnet_mask = None
 
     def may_be_subnet(self):
         """Could this be a subnet that MAAS is interested in?"""
         return all([
             self.interface != 'lo',
             self.ip is not None,
-            self.mask is not None,
+            self.subnet_mask is not None,
             ])
 
     def as_dict(self):
+        """Return information as a dictionary.
+
+        The return value's format is suitable as an interface description
+        for use with the `register` API call.
+        """
         return {
             'interface': self.interface,
             'ip': self.ip,
-            'mask': self.mask,
+            'subnet_mask': self.subnet_mask,
         }
 
 
 def run_ifconfig():
     """Run `ifconfig` to list active interfaces.  Return output."""
     env = dict(os.environ, LC_ALL='C')
-    stdout = BytesIO()
-    check_call(['/sbin/ifconfig'], env=env, stdout=stdout)
-    stdout.seek(0)
-    return stdout.read().decode('ascii')
-
-
-def extract_ip_and_mask(line):
+    output = check_output(['/sbin/ifconfig'], env=env)
+    return output.decode('ascii')
+
+
+def extract_ip_and_subnet_mask(line):
     """Get IP address and subnet mask from an inet address line."""
     # This line consists of key:value pairs separated by double spaces.
     # The "inet addr" key contains a space.  There is typically a
@@ -80,7 +82,7 @@
     info = InterfaceInfo(header.split()[0])
     for line in lines[1:]:
         if line.split()[0] == 'inet':
-            info.ip, info.mask = extract_ip_and_mask(line)
+            info.ip, info.subnet_mask = extract_ip_and_subnet_mask(line)
     return info
 
 
@@ -100,6 +102,6 @@
     """Find the networks attached to this system."""
     output = run_ifconfig()
     return [
-        interface
+        interface.as_dict()
         for interface in parse_ifconfig(output)
             if interface.may_be_subnet()]

=== modified file 'src/provisioningserver/start_cluster_controller.py'
--- src/provisioningserver/start_cluster_controller.py	2012-09-25 17:26:46 +0000
+++ src/provisioningserver/start_cluster_controller.py	2012-09-26 17:47:23 +0000
@@ -31,6 +31,7 @@
     NoAuth,
     )
 from provisioningserver.logging import task_logger
+from provisioningserver.network import discover_networks
 
 
 class ClusterControllerRejected(Exception):
@@ -41,6 +42,8 @@
     """For use by :class:`MainScript`."""
     parser.add_argument(
         'server_url', metavar='URL', help="URL to the MAAS region controller.")
+    parser.add_argument(
+        '--uuid', metavar='UUID', help="UUID for this cluster controller.")
 
 
 def log_error(exception):
@@ -54,12 +57,14 @@
     return MAASClient(NoAuth(), MAASDispatcher(), server_url)
 
 
-def register(server_url):
+def register(server_url, uuid):
     """Request Rabbit connection details from the domain controller.
 
     Offers this machine to the region controller as a potential cluster
     controller.
 
+    :param server_url: URL to the region controller's MAAS API.
+    :param uuid: UUID for this cluster controller.
     :return: A dict of connection details if this cluster controller has been
         accepted, or `None` if there is no definite response yet.  If there
         is no definite response, retry this call later.
@@ -67,9 +72,13 @@
         cluster controller.
     """
     known_responses = {httplib.OK, httplib.FORBIDDEN, httplib.ACCEPTED}
+
+    interfaces = json.dumps(discover_networks())
     client = make_anonymous_api_client(server_url)
     try:
-        response = client.post('api/1.0/nodegroups/', 'register')
+        response = client.post(
+            'api/1.0/nodegroups/', 'register',
+            uuid=uuid, interfaces=interfaces)
     except HTTPError as e:
         status_code = e.code
         if e.code not in known_responses:
@@ -150,7 +159,7 @@
     If this system is still awaiting approval as a cluster controller, this
     command will keep looping until it gets a definite answer.
     """
-    connection_details = register(args.server_url)
+    connection_details = register(args.server_url, args.uuid)
     while connection_details is None:
         sleep(60)
         connection_details = register(args.server_url)

=== modified file 'src/provisioningserver/tests/test_network.py'
--- src/provisioningserver/tests/test_network.py	2012-09-26 17:47:23 +0000
+++ src/provisioningserver/tests/test_network.py	2012-09-26 17:47:23 +0000
@@ -22,31 +22,18 @@
 from provisioningserver import network
 
 
-class FakeCheckCall:
-    """Test double for `check_call`."""
-
-    def __init__(self, output_text):
-        self.output_text = output_text
-        self.calls = []
-
-    def __call__(self, command, stdout=None, env=None):
-        stdout.write(self.output_text.encode('ascii'))
-        self.calls.append(dict(command=command, env=env))
-        return 0
-
-
 def make_address_line(**kwargs):
     """Create an inet address line."""
     # First word on this line is inet or inet6.
     kwargs.setdefault('inet', 'inet')
     kwargs.setdefault('broadcast', '10.255.255.255')
-    kwargs.setdefault('mask', '255.0.0.0')
+    kwargs.setdefault('subnet_mask', '255.0.0.0')
     items = [
         "%(inet)s addr:%(ip)s"
         ]
     if len(kwargs['broadcast']) > 0:
         items.append("Bcast:%(broadcast)s")
-    items.append("Mask:%(mask)s")
+    items.append("Mask:%(subnet_mask)s")
     return '  '.join(items) % kwargs
 
 
@@ -189,7 +176,7 @@
 
     def test_run_ifconfig_returns_ifconfig_output(self):
         text = join_stanzas([make_stanza()])
-        self.patch(network, 'check_call', FakeCheckCall(text))
+        self.patch(network, 'check_output').return_value = text.encode('ascii')
         self.assertEqual(text, network.run_ifconfig())
 
     def test_parse_ifconfig_produces_interface_info(self):
@@ -205,7 +192,7 @@
         parms = {
             'interface': factory.make_name('eth'),
             'ip': factory.getRandomIPAddress(),
-            'mask': '255.255.255.128',
+            'subnet_mask': '255.255.255.128',
         }
         info = network.parse_stanza(make_stanza(**parms))
         self.assertEqual(parms, info.as_dict())
@@ -214,7 +201,7 @@
         parms = {
             'interface': factory.make_name('eth'),
             'ip': factory.getRandomIPAddress(),
-            'mask': '255.255.255.128',
+            'subnet_mask': '255.255.255.128',
             'interrupt': '',
         }
         info = network.parse_stanza(make_stanza(**parms))
@@ -230,14 +217,14 @@
         info = network.parse_stanza(make_stanza(**parms))
         expected = parms.copy()
         expected['ip'] = None
-        expected['mask'] = None
+        expected['subnet_mask'] = None
         self.assertEqual(expected, info.as_dict())
 
     def test_parse_stanza_returns_nothing_for_loopback(self):
         parms = {
             'interface': 'lo',
             'ip': '127.1.2.3',
-            'mask': '255.0.0.0',
+            'subnet_mask': '255.0.0.0',
             'encapsulation': 'Local Loopback',
             'broadcast': '',
             'interrupt': '',
@@ -258,17 +245,21 @@
             [stanza.strip() for stanza in stanzas],
             network.split_stanzas(full_output))
 
+    def test_discover_networks_runs_in_real_life(self):
+        interfaces = network.discover_networks()
+        self.assertIsInstance(interfaces, list)
+
     def test_discover_networks_returns_suitable_interfaces(self):
         params = {
             'interface': factory.make_name('eth'),
             'ip': factory.getRandomIPAddress(),
-            'mask': '255.255.255.0',
+            'subnet_mask': '255.255.255.0',
         }
         regular_interface = make_stanza(**params)
         loopback = make_stanza(
             interface='lo', encapsulation='Local loopback', broadcast='',
             interrupt='')
-        disabled_interface = make_stanza(ip='', broadcast='', mask='')
+        disabled_interface = make_stanza(ip='', broadcast='', subnet_mask='')
 
         text = join_stanzas([regular_interface, loopback, disabled_interface])
         self.patch(network, 'run_ifconfig').return_value = text
@@ -277,11 +268,11 @@
 
         self.assertEqual(
             [params],
-            [interface.as_dict() for interface in interfaces])
+            [interface for interface in interfaces])
 
     def test_discover_networks_processes_real_ifconfig_output(self):
         self.patch(network, 'run_ifconfig').return_value = sample_output
         info = network.discover_networks()
         self.assertEqual(
             ['eth1', 'maasbr0', 'virbr0'],
-            [interface.interface for interface in info])
+            [interface['interface'] for interface in info])

=== modified file 'src/provisioningserver/tests/test_start_cluster_controller.py'
--- src/provisioningserver/tests/test_start_cluster_controller.py	2012-09-25 17:26:46 +0000
+++ src/provisioningserver/tests/test_start_cluster_controller.py	2012-09-26 17:47:23 +0000
@@ -42,7 +42,22 @@
     """
 
 
-FakeArgs = namedtuple('FakeArgs', ['server_url'])
+def make_url(name_hint='host'):
+    return "http://%s.example.com/%s/"; % (
+        factory.make_name(name_hint),
+        factory.make_name('path'),
+        )
+
+
+FakeArgs = namedtuple('FakeArgs', ['server_url', 'uuid'])
+
+
+def make_args(server_url=None, uuid=None):
+    if server_url is None:
+        server_url = make_url('region')
+    if uuid is None:
+        uuid = factory.getRandomUUID()
+    return FakeArgs(server_url, uuid)
 
 
 class FakeURLOpenResponse:
@@ -59,13 +74,6 @@
         return self._status_code
 
 
-def make_url(name_hint='host'):
-    return "http://%s.example.com/%s/"; % (
-        factory.make_name(name_hint),
-        factory.make_name('path'),
-        )
-
-
 class TestStartClusterController(PservTestCase):
 
     def setUp(self):
@@ -79,6 +87,20 @@
             'BROKER_URL': make_url('broker'),
         }
 
+    def parse_headers_and_body(self, headers, body):
+        """Parse ingredients of a web request.
+
+        The headers and body are as passed to :class:`MAASDispatcher`.
+        """
+        # Make Django STFU; just using Django's multipart code causes it to
+        # pull in a settings module, and it will throw up if it can't.
+        self.useFixture(
+            EnvironmentVariableFixture(
+                "DJANGO_SETTINGS_MODULE", __name__))
+
+        post, files = parse_headers_and_body_with_django(headers, body)
+        return post, files
+
     def prepare_response(self, http_code, content=""):
         """Prepare to return the given http response from API request."""
         fake = self.patch(MAASDispatcher, 'dispatch_query')
@@ -108,14 +130,17 @@
         self.prepare_success_response()
         parser = ArgumentParser()
         start_cluster_controller.add_arguments(parser)
-        start_cluster_controller.run(parser.parse_args((make_url(), )))
+        start_cluster_controller.run(parser.parse_args((
+            make_url(),
+            '--uuid', factory.getRandomUUID(),
+            )))
         self.assertNotEqual(0, start_cluster_controller.Popen.call_count)
 
     def test_uses_given_url(self):
         url = make_url('region')
         self.patch(start_cluster_controller, 'start_up')
         self.prepare_success_response()
-        start_cluster_controller.run(FakeArgs(url))
+        start_cluster_controller.run(make_args(server_url=url))
         (args, kwargs) = MAASDispatcher.dispatch_query.call_args
         self.assertEqual(url + 'api/1.0/nodegroups/', args[0])
 
@@ -124,7 +149,7 @@
         self.prepare_rejection_response()
         self.assertRaises(
             start_cluster_controller.ClusterControllerRejected,
-            start_cluster_controller.run, FakeArgs(make_url()))
+            start_cluster_controller.run, make_args())
         self.assertItemsEqual([], start_cluster_controller.start_up.calls_list)
 
     def test_polls_while_pending(self):
@@ -132,7 +157,7 @@
         self.prepare_pending_response()
         self.assertRaises(
             Sleeping,
-            start_cluster_controller.run, FakeArgs(make_url()))
+            start_cluster_controller.run, make_args())
         self.assertItemsEqual([], start_cluster_controller.start_up.calls_list)
 
     def test_polls_on_unexpected_errors(self):
@@ -141,14 +166,33 @@
             make_url(), httplib.REQUEST_TIMEOUT, "Timeout.", '', BytesIO())
         self.assertRaises(
             Sleeping,
-            start_cluster_controller.run, FakeArgs(make_url()))
+            start_cluster_controller.run, make_args())
         self.assertItemsEqual([], start_cluster_controller.start_up.calls_list)
 
+    def test_register_passes_cluster_information(self):
+        self.prepare_success_response()
+        uuid = factory.getRandomUUID()
+        interface = {
+            'interface': factory.make_name('eth'),
+            'ip': factory.getRandomIPAddress(),
+            'subnet_mask': '255.255.255.0',
+            }
+        discover = self.patch(start_cluster_controller, 'discover_networks')
+        discover.return_value = [interface]
+
+        start_cluster_controller.register(make_url(), uuid)
+
+        (args, kwargs) = MAASDispatcher.dispatch_query.call_args
+        headers, body = kwargs["headers"], kwargs["data"]
+        post, files = self.parse_headers_and_body(headers, body)
+        self.assertEqual([interface], json.loads(post['interfaces']))
+        self.assertEqual(uuid, post['uuid'])
+
     def test_starts_up_once_accepted(self):
         self.patch(start_cluster_controller, 'start_up')
         connection_details = self.prepare_success_response()
         server_url = make_url()
-        start_cluster_controller.run(FakeArgs(server_url))
+        start_cluster_controller.run(make_args(server_url=server_url))
         start_cluster_controller.start_up.assert_called_once_with(
             server_url, connection_details)
 
@@ -157,7 +201,7 @@
         connection_details = self.make_connection_details()
         self.patch(start_cluster_controller, 'Popen')
         self.patch(start_cluster_controller, 'sleep')
-        self.prepare_response('OK', httplib.OK)
+        self.prepare_success_response()
 
         start_cluster_controller.start_up(url, connection_details)
 
@@ -165,14 +209,8 @@
         self.assertEqual(url + 'api/1.0/nodegroups/', args[0])
         self.assertEqual('POST', kwargs['method'])
 
-        # Make Django STFU; just using Django's multipart code causes it to
-        # pull in a settings module, and it will throw up if it can't.
-        self.useFixture(
-            EnvironmentVariableFixture(
-                "DJANGO_SETTINGS_MODULE", __name__))
-
         headers, body = kwargs["headers"], kwargs["data"]
-        post, files = parse_headers_and_body_with_django(headers, body)
+        post, files = self.parse_headers_and_body(headers, body)
         self.assertEqual("refresh_workers", post["op"])
 
     def test_start_up_ignores_failure_on_refresh_secrets(self):