Merge lp:~racb/maas/arch-detect into lp:~maas-committers/maas/trunk

Proposed by Robie Basak
Status: Merged
Approved by: Robie Basak
Approved revision: no longer in the source branch.
Merged at revision: 1142
Proposed branch: lp:~racb/maas/arch-detect
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 371 lines (+175/-45)
4 files modified
src/maasserver/api.py (+66/-12)
src/maasserver/tests/test_api.py (+36/-25)
src/provisioningserver/tests/test_tftp.py (+30/-3)
src/provisioningserver/tftp.py (+43/-5)
To merge this branch: bzr merge lp:~racb/maas/arch-detect
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+127458@code.launchpad.net

Commit message

Add architecture detection

Rather than always responding to pxelinux.cfg/01-<mac> dynamically, instead cause nodes to fall back to pxelinux.cfg/default if the architecture of the machine with the provided MAC address is unknown. This allows non-Intel machines to fall back to pxelinux.cfg/default.<arch>-<subarch> in the middle, at which point serve a configuration for the correct non-Intel architecture.

This allows multiple architectures to be served the correct pxelinux.cfg configuration for enlistment without any needed intervention.

See bug 1041092 for details on pxelinux.cfg/default.<arch>-<subarch>.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Several minor comments, but they're mostly about style; the approach
is good, afaict.

[1]

+    if macaddress:
+        return macaddress.node
+    else:
+        return None

Don't know how you feel about conditional expressions, but this might
be as clear but more concise as:

    return macaddress.node if macaddress else None

[2]

+    :param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not
+                 'armhf').

Typically we align follow-on lines 4-spaces in from the first
line. Yay, another trivial nitpick :)

[3]

+                # Request was pxelinux.cfg/01-<mac>, so attempt fall back
+                # to pxelinux.cfg/default-<arch> for arch detection.

Is it default-<arch> or default.<arch>?

Update: the regex says either.

[4]

+            if pxelinux_arch == 'arm':
+                arch = 'armhf'
+            else:
+                arch = pxelinux_arch

One step in the direction of a lookup-table, and more concise:

            arch = {"arm": "armhf"}.get(pxelinux_arch, pxelinux_arch)

Er, but it's not very pretty. Never mind. Why is "arm" being sent
instead of "armhf"? Should we normalise on just "arm"?

[5]

+        response = self.client.get(reverse('pxeconfig'),
+                                   self.get_default_params())

We have followed the style of wrapping 4 spaces in most places:

        response = self.client.get(
            reverse('pxeconfig'), self.get_default_params())

Also, when we need to break onto a newline, we break at the opening
brace, so not like this:

        response = self.client.get(reverse('pxeconfig'),
            self.get_default_params())

Too much code on Launchpad ended up in the rightmost 20 columns
(there, like here, we chose to use a right margin) so we changed the
convention.

Hardly needs saying, but Emacs' python-mode does this all perfectly,
right out of the box ;)

[6]

+        ( # either a MAC

Make this a non-capturing group, i.e. (?:...)

+            {htype:02x}    # ARP HTYPE.
+            -
+            (?P<mac>{re_mac_address.pattern})    # Capture MAC.
+        | # or "default"
+            default
+              ( # perhaps with specified arch

Here too?

+                [.-](?P<arch>\w+) # arch
+                (-(?P<subarch>\w+))? # optional subarch

And here?

+              )?

[7]

+            params = {k: v
+                      for k, v in config_file_match.groupdict().iteritems()
+                      if v is not None}

We have tended to avoid single-letter variable names (sorry, again,
this is Launchpad engineering heritage showing its face, along with my
pedantry). Also, we decided to use the non-iter methods on dicts, I
think because they're spelled the same as in Python 3. I preferred to
use them, because they behave differently, and 2to3 translates them
anyway, but we, Red Squad, in days of old, agreed this. So, we'd write
this as:

            params = {
                key: value
                for key, value in config_file_match.groupdict().items()
                if value is not None
                }

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

>                                                    ... I preferred to
> use them, because they behave differently, and 2to3 translates them
> anyway, but we, Red Squad, in days of old, agreed this. ...

There's a "not" missing from the start of the second line there, but
you probably guessed that.

Revision history for this message
Robie Basak (racb) wrote :

[1] Done
[2] Done
[3] Clarified separator in corresponding comments

[4]

> Why is "arm" being sent instead of "armhf"? Should we normalise on just "arm"?

Outside userspace, the "hf" part of armhf has no meaning as there is no defined userspace ABI at that point. So before userspace, armel and armhf are identical. But MAAS does care about userspace. albeit only armhf userspace for ARM, because it needs to know which userspace to deploy. So pxelinux emulators should be agnostic of el/hf yet MAAS must be aware of it. So we must map it.

[5] Done
[6] Done
[7] Done

Thanks Gavin!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2012-10-03 02:28:04 +0000
+++ src/maasserver/api.py 2012-10-03 08:57:21 +0000
@@ -1496,33 +1496,87 @@
1496 return "poweroff"1496 return "poweroff"
14971497
14981498
1499def get_node_from_mac_string(mac_string):
1500 """Get a Node object from a MAC address string.
1501
1502 Returns a Node object or None if no node with the given MAC address exists.
1503
1504 :param mac_string: MAC address string in the form "12-34-56-78-9a-bc"
1505 :return: Node object or None
1506 """
1507 if mac_string is None:
1508 return None
1509 macaddress = get_one(MACAddress.objects.filter(mac_address=mac_string))
1510 return macaddress.node if macaddress else None
1511
1512
1499def pxeconfig(request):1513def pxeconfig(request):
1500 """Get the PXE configuration given a node's details.1514 """Get the PXE configuration given a node's details.
15011515
1502 Returns a JSON object corresponding to a1516 Returns a JSON object corresponding to a
1503 :class:`provisioningserver.kernel_opts.KernelParameters` instance.1517 :class:`provisioningserver.kernel_opts.KernelParameters` instance.
15041518
1519 This is now fairly decoupled from pxelinux's TFTP filename encoding
1520 mechanism, with one notable exception. Call this function with (mac, arch,
1521 subarch) and it will do the right thing. If details it needs are missing
1522 (ie. arch/subarch missing when the MAC is supplied but unknown), then it
1523 will as an exception return an HTTP NO_CONTENT (204) in the expectation
1524 that this will be translated to a TFTP file not found and pxelinux (or an
1525 emulator) will fall back to default-<arch>-<subarch> (in the case of an
1526 alternate architecture emulator) or just straight to default (in the case
1527 of native pxelinux on i386 or amd64). See bug 1041092 for details and
1528 discussion.
1529
1505 :param mac: MAC address to produce a boot configuration for.1530 :param mac: MAC address to produce a boot configuration for.
1531 :param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not
1532 'armhf').
1533 :param subarch: Subarchitecture name (in the pxelinux namespace).
1506 """1534 """
1507 mac = get_mandatory_param(request.GET, 'mac')1535 node = get_node_from_mac_string(request.GET.get('mac', None))
15081536
1509 macaddress = get_one(MACAddress.objects.filter(mac_address=mac))1537 if node:
1510 if macaddress is None:
1511 # Default to i386 as a works-for-all solution. This will not support
1512 # non-x86 architectures, but for now this assumption holds.
1513 node = None
1514 arch, subarch = ARCHITECTURE.i386.split('/')
1515 preseed_url = compose_enlistment_preseed_url()
1516 hostname = 'maas-enlist'
1517 domain = Config.objects.get_config('enlistment_domain')
1518 else:
1519 node = macaddress.node
1520 arch, subarch = node.architecture.split('/')1538 arch, subarch = node.architecture.split('/')
1521 preseed_url = compose_preseed_url(node)1539 preseed_url = compose_preseed_url(node)
1522 # The node's hostname may include a domain, but we ignore that1540 # The node's hostname may include a domain, but we ignore that
1523 # and use the one from the nodegroup instead.1541 # and use the one from the nodegroup instead.
1524 hostname = node.hostname.split('.', 1)[0]1542 hostname = node.hostname.split('.', 1)[0]
1525 domain = node.nodegroup.name1543 domain = node.nodegroup.name
1544 else:
1545 try:
1546 pxelinux_arch = request.GET['arch']
1547 except KeyError:
1548 if 'mac' in request.GET:
1549 # Request was pxelinux.cfg/01-<mac>, so attempt fall back
1550 # to pxelinux.cfg/default-<arch>-<subarch> for arch detection.
1551 return HttpResponse(status=httplib.NO_CONTENT)
1552 else:
1553 # Request has already fallen back, so if arch is still not
1554 # provided then use i386.
1555 arch = ARCHITECTURE.i386.split('/')[0]
1556 else:
1557 # Map from pxelinux namespace architecture names to MAAS namespace
1558 # architecture names. If this gets bigger, an external lookup table
1559 # would make sense. But here is fine for something as trivial as it
1560 # is right now.
1561 if pxelinux_arch == 'arm':
1562 arch = 'armhf'
1563 else:
1564 arch = pxelinux_arch
1565
1566 # Use subarch if supplied; otherwise assume 'generic'.
1567 try:
1568 pxelinux_subarch = request.GET['subarch']
1569 except KeyError:
1570 subarch = 'generic'
1571 else:
1572 # Map from pxelinux namespace subarchitecture names to MAAS
1573 # namespace subarchitecture names. Right now this happens to be a
1574 # 1-1 mapping.
1575 subarch = pxelinux_subarch
1576
1577 preseed_url = compose_enlistment_preseed_url()
1578 hostname = 'maas-enlist'
1579 domain = Config.objects.get_config('enlistment_domain')
15261580
1527 if node is None or node.status == NODE_STATUS.COMMISSIONING:1581 if node is None or node.status == NODE_STATUS.COMMISSIONING:
1528 series = Config.objects.get_config('commissioning_distro_series')1582 series = Config.objects.get_config('commissioning_distro_series')
15291583
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2012-10-03 02:28:04 +0000
+++ src/maasserver/tests/test_api.py 2012-10-03 08:57:21 +0000
@@ -2715,21 +2715,22 @@
27152715
2716class TestPXEConfigAPI(AnonAPITestCase):2716class TestPXEConfigAPI(AnonAPITestCase):
27172717
2718 def get_params(self):2718 def get_mac_params(self):
2719 return {'mac': factory.make_mac_address().mac_address}2719 return {'mac': factory.make_mac_address().mac_address}
27202720
2721 def get_optional_params(self):2721 def get_default_params(self):
2722 return ['mac']2722 return dict()
27232723
2724 def get_pxeconfig(self, params=None):2724 def get_pxeconfig(self, params=None):
2725 """Make a request to `pxeconfig`, and return its response dict."""2725 """Make a request to `pxeconfig`, and return its response dict."""
2726 if params is None:2726 if params is None:
2727 params = self.get_params()2727 params = self.get_default_params()
2728 response = self.client.get(reverse('pxeconfig'), params)2728 response = self.client.get(reverse('pxeconfig'), params)
2729 return json.loads(response.content)2729 return json.loads(response.content)
27302730
2731 def test_pxeconfig_returns_json(self):2731 def test_pxeconfig_returns_json(self):
2732 response = self.client.get(reverse('pxeconfig'), self.get_params())2732 response = self.client.get(
2733 reverse('pxeconfig'), self.get_default_params())
2733 self.assertThat(2734 self.assertThat(
2734 (2735 (
2735 response.status_code,2736 response.status_code,
@@ -2751,7 +2752,28 @@
2751 self.get_pxeconfig(),2752 self.get_pxeconfig(),
2752 ContainsAll(KernelParameters._fields))2753 ContainsAll(KernelParameters._fields))
27532754
2754 def test_pxeconfig_defaults_to_i386_when_node_unknown(self):2755 def test_pxeconfig_returns_data_for_known_node(self):
2756 params = self.get_mac_params()
2757 node = MACAddress.objects.get(mac_address=params['mac']).node
2758 response = self.client.get(reverse('pxeconfig'), params)
2759 self.assertEqual(httplib.OK, response.status_code)
2760
2761 def test_pxeconfig_returns_no_content_for_unknown_node(self):
2762 params = dict(mac=factory.getRandomMACAddress(delimiter=b'-'))
2763 response = self.client.get(reverse('pxeconfig'), params)
2764 self.assertEqual(httplib.NO_CONTENT, response.status_code)
2765
2766 def test_pxeconfig_returns_data_for_detailed_but_unknown_node(self):
2767 architecture = factory.getRandomEnum(ARCHITECTURE)
2768 arch, subarch = architecture.split('/')
2769 params = dict(
2770 mac=factory.getRandomMACAddress(delimiter=b'-'),
2771 arch=arch,
2772 subarch=subarch)
2773 response = self.client.get(reverse('pxeconfig'), params)
2774 self.assertEqual(httplib.OK, response.status_code)
2775
2776 def test_pxeconfig_defaults_to_i386_for_default(self):
2755 # As a lowest-common-denominator, i386 is chosen when the node is not2777 # As a lowest-common-denominator, i386 is chosen when the node is not
2756 # yet known to MAAS.2778 # yet known to MAAS.
2757 expected_arch = tuple(ARCHITECTURE.i386.split('/'))2779 expected_arch = tuple(ARCHITECTURE.i386.split('/'))
@@ -2760,16 +2782,12 @@
2760 self.assertEqual(expected_arch, observed_arch)2782 self.assertEqual(expected_arch, observed_arch)
27612783
2762 def test_pxeconfig_uses_fixed_hostname_for_enlisting_node(self):2784 def test_pxeconfig_uses_fixed_hostname_for_enlisting_node(self):
2763 new_mac = factory.getRandomMACAddress()2785 self.assertEqual('maas-enlist', self.get_pxeconfig().get('hostname'))
2764 self.assertEqual(
2765 'maas-enlist',
2766 self.get_pxeconfig({'mac': new_mac}).get('hostname'))
27672786
2768 def test_pxeconfig_uses_enlistment_domain_for_enlisting_node(self):2787 def test_pxeconfig_uses_enlistment_domain_for_enlisting_node(self):
2769 new_mac = factory.getRandomMACAddress()
2770 self.assertEqual(2788 self.assertEqual(
2771 Config.objects.get_config('enlistment_domain'),2789 Config.objects.get_config('enlistment_domain'),
2772 self.get_pxeconfig({'mac': new_mac}).get('domain'))2790 self.get_pxeconfig().get('domain'))
27732791
2774 def test_pxeconfig_splits_domain_from_node_hostname(self):2792 def test_pxeconfig_splits_domain_from_node_hostname(self):
2775 host = factory.make_name('host')2793 host = factory.make_name('host')
@@ -2800,24 +2818,16 @@
2800 kernel_opts, 'get_ephemeral_name',2818 kernel_opts, 'get_ephemeral_name',
2801 FakeMethod(result=factory.getRandomString()))2819 FakeMethod(result=factory.getRandomString()))
28022820
2803 def test_pxeconfig_requires_mac_address(self):2821 def test_pxeconfig_has_enlistment_preseed_url_for_default(self):
2804 # The `mac` parameter is mandatory.2822 self.silence_get_ephemeral_name()
2805 self.silence_get_ephemeral_name()2823 params = self.get_default_params()
2806 self.assertEqual(
2807 httplib.BAD_REQUEST,
2808 self.get_without_param("mac").status_code)
2809
2810 def test_pxeconfig_has_enlistment_preseed_url_for_unknown_node(self):
2811 self.silence_get_ephemeral_name()
2812 params = self.get_params()
2813 params['mac'] = factory.getRandomMACAddress()
2814 response = self.client.get(reverse('pxeconfig'), params)2824 response = self.client.get(reverse('pxeconfig'), params)
2815 self.assertEqual(2825 self.assertEqual(
2816 compose_enlistment_preseed_url(),2826 compose_enlistment_preseed_url(),
2817 json.loads(response.content)["preseed_url"])2827 json.loads(response.content)["preseed_url"])
28182828
2819 def test_pxeconfig_has_preseed_url_for_known_node(self):2829 def test_pxeconfig_has_preseed_url_for_known_node(self):
2820 params = self.get_params()2830 params = self.get_mac_params()
2821 node = MACAddress.objects.get(mac_address=params['mac']).node2831 node = MACAddress.objects.get(mac_address=params['mac']).node
2822 response = self.client.get(reverse('pxeconfig'), params)2832 response = self.client.get(reverse('pxeconfig'), params)
2823 self.assertEqual(2833 self.assertEqual(
@@ -2852,7 +2862,8 @@
2852 def test_pxeconfig_uses_boot_purpose(self):2862 def test_pxeconfig_uses_boot_purpose(self):
2853 fake_boot_purpose = factory.make_name("purpose")2863 fake_boot_purpose = factory.make_name("purpose")
2854 self.patch(api, "get_boot_purpose", lambda node: fake_boot_purpose)2864 self.patch(api, "get_boot_purpose", lambda node: fake_boot_purpose)
2855 response = self.client.get(reverse('pxeconfig'), self.get_params())2865 response = self.client.get(reverse('pxeconfig'),
2866 self.get_default_params())
2856 self.assertEqual(2867 self.assertEqual(
2857 fake_boot_purpose,2868 fake_boot_purpose,
2858 json.loads(response.content)["purpose"])2869 json.loads(response.content)["purpose"])
28592870
=== modified file 'src/provisioningserver/tests/test_tftp.py'
--- src/provisioningserver/tests/test_tftp.py 2012-08-30 10:37:26 +0000
+++ src/provisioningserver/tests/test_tftp.py 2012-10-03 08:57:21 +0000
@@ -70,7 +70,9 @@
70 The path is intended to match `re_config_file`, and the components are70 The path is intended to match `re_config_file`, and the components are
71 the expected groups from a match.71 the expected groups from a match.
72 """72 """
73 components = {"mac": factory.getRandomMACAddress(b"-")}73 components = {"mac": factory.getRandomMACAddress(b"-"),
74 "arch": None,
75 "subarch": None}
74 config_path = compose_config_path(components["mac"])76 config_path = compose_config_path(components["mac"])
75 return config_path, components77 return config_path, components
7678
@@ -112,13 +114,15 @@
112 mac = 'aa-bb-cc-dd-ee-ff'114 mac = 'aa-bb-cc-dd-ee-ff'
113 match = TFTPBackend.re_config_file.match('pxelinux.cfg/01-%s' % mac)115 match = TFTPBackend.re_config_file.match('pxelinux.cfg/01-%s' % mac)
114 self.assertIsNotNone(match)116 self.assertIsNotNone(match)
115 self.assertEqual({'mac': mac}, match.groupdict())117 self.assertEqual({'mac': mac, 'arch': None, 'subarch': None},
118 match.groupdict())
116119
117 def test_re_config_file_matches_pxelinux_cfg_with_leading_slash(self):120 def test_re_config_file_matches_pxelinux_cfg_with_leading_slash(self):
118 mac = 'aa-bb-cc-dd-ee-ff'121 mac = 'aa-bb-cc-dd-ee-ff'
119 match = TFTPBackend.re_config_file.match('/pxelinux.cfg/01-%s' % mac)122 match = TFTPBackend.re_config_file.match('/pxelinux.cfg/01-%s' % mac)
120 self.assertIsNotNone(match)123 self.assertIsNotNone(match)
121 self.assertEqual({'mac': mac}, match.groupdict())124 self.assertEqual({'mac': mac, 'arch': None, 'subarch': None},
125 match.groupdict())
122126
123 def test_re_config_file_does_not_match_non_config_file(self):127 def test_re_config_file_does_not_match_non_config_file(self):
124 self.assertIsNone(128 self.assertIsNone(
@@ -132,6 +136,29 @@
132 self.assertIsNone(136 self.assertIsNone(
133 TFTPBackend.re_config_file.match('foo/01-aa-bb-cc-dd-ee-ff'))137 TFTPBackend.re_config_file.match('foo/01-aa-bb-cc-dd-ee-ff'))
134138
139 def test_re_config_file_with_default(self):
140 match = TFTPBackend.re_config_file.match('pxelinux.cfg/default')
141 self.assertIsNotNone(match)
142 self.assertEqual({'mac': None, 'arch': None, 'subarch': None},
143 match.groupdict())
144
145 def test_re_config_file_with_default_arch(self):
146 arch = factory.make_name('arch', sep='')
147 match = TFTPBackend.re_config_file.match('pxelinux.cfg/default.%s' %
148 arch)
149 self.assertIsNotNone(match)
150 self.assertEqual({'mac': None, 'arch': arch, 'subarch': None},
151 match.groupdict())
152
153 def test_re_config_file_with_default_arch_and_subarch(self):
154 arch = factory.make_name('arch', sep='')
155 subarch = factory.make_name('subarch', sep='')
156 match = TFTPBackend.re_config_file.match(
157 'pxelinux.cfg/default.%s-%s' % (arch, subarch))
158 self.assertIsNotNone(match)
159 self.assertEqual({'mac': None, 'arch': arch, 'subarch': subarch},
160 match.groupdict())
161
135162
136class TestTFTPBackend(TestCase):163class TestTFTPBackend(TestCase):
137 """Tests for `provisioningserver.tftp.TFTPBackend`."""164 """Tests for `provisioningserver.tftp.TFTPBackend`."""
138165
=== modified file 'src/provisioningserver/tftp.py'
--- src/provisioningserver/tftp.py 2012-09-12 19:56:23 +0000
+++ src/provisioningserver/tftp.py 2012-10-03 08:57:21 +0000
@@ -14,6 +14,7 @@
14 "TFTPBackend",14 "TFTPBackend",
15 ]15 ]
1616
17import httplib
17from io import BytesIO18from io import BytesIO
18from itertools import repeat19from itertools import repeat
19import json20import json
@@ -32,7 +33,9 @@
32 FilesystemSynchronousBackend,33 FilesystemSynchronousBackend,
33 IReader,34 IReader,
34 )35 )
36from tftp.errors import FileNotFound
35from twisted.web.client import getPage37from twisted.web.client import getPage
38import twisted.web.error
36from zope.interface import implementer39from zope.interface import implementer
3740
3841
@@ -89,9 +92,18 @@
89 ^/*92 ^/*
90 pxelinux[.]cfg # PXELINUX expects this.93 pxelinux[.]cfg # PXELINUX expects this.
91 /94 /
92 {htype:02x} # ARP HTYPE.95 (?: # either a MAC
93 -96 {htype:02x} # ARP HTYPE.
94 (?P<mac>{re_mac_address.pattern}) # Capture MAC.97 -
98 (?P<mac>{re_mac_address.pattern}) # Capture MAC.
99 | # or "default"
100 default
101 (?: # perhaps with specified arch, with a separator of either '-'
102 # or '.', since the spec was changed and both are unambiguous
103 [.-](?P<arch>\w+) # arch
104 (?:-(?P<subarch>\w+))? # optional subarch
105 )?
106 )
95 $107 $
96 '''.format(108 '''.format(
97 htype=ARP_HTYPE.ETHERNET,109 htype=ARP_HTYPE.ETHERNET,
@@ -162,6 +174,25 @@
162 d.addCallback(BytesReader)174 d.addCallback(BytesReader)
163 return d175 return d
164176
177 @staticmethod
178 def get_page_errback(failure, file_name):
179 failure.trap(twisted.web.error.Error)
180 # This twisted.web.error.Error.status object ends up being a
181 # string for some reason, but the constants we can compare against
182 # (both in httplib and twisted.web.http) are ints.
183 try:
184 status_int = int(failure.value.status)
185 except ValueError:
186 # Assume that it's some other error and propagate it
187 return failure
188
189 if status_int == httplib.NO_CONTENT:
190 # Convert HTTP No Content to a TFTP file not found
191 raise FileNotFound(file_name)
192 else:
193 # Otherwise propogate the unknown error
194 return failure
195
165 @deferred196 @deferred
166 def get_reader(self, file_name):197 def get_reader(self, file_name):
167 """See `IBackend.get_reader()`.198 """See `IBackend.get_reader()`.
@@ -174,5 +205,12 @@
174 if config_file_match is None:205 if config_file_match is None:
175 return super(TFTPBackend, self).get_reader(file_name)206 return super(TFTPBackend, self).get_reader(file_name)
176 else:207 else:
177 params = config_file_match.groupdict()208 # Do not include any element that has not matched (ie. is None)
178 return self.get_config_reader(params)209 params = {
210 key: value
211 for key, value in config_file_match.groupdict().items()
212 if value is not None
213 }
214 d = self.get_config_reader(params)
215 d.addErrback(self.get_page_errback, file_name)
216 return d