Merge ~mpontillo/maas:better-default-maas-url--bug-1418044 into maas:master

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: c01a722aaabd8cd3e028d1548c580018532655cd
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~mpontillo/maas:better-default-maas-url--bug-1418044
Merge into: maas:master
Diff against target: 1158 lines (+322/-117)
11 files modified
src/maasserver/compose_preseed.py (+66/-35)
src/maasserver/preseed.py (+62/-35)
src/maasserver/rpc/boot.py (+9/-3)
src/maasserver/rpc/tests/test_boot.py (+7/-3)
src/maasserver/server_address.py (+5/-1)
src/maasserver/tests/test_compose_preseed.py (+101/-16)
src/maasserver/tests/test_preseed.py (+8/-5)
src/maasserver/utils/__init__.py (+14/-6)
src/metadataserver/api.py (+28/-8)
src/metadataserver/tests/test_api.py (+19/-3)
src/provisioningserver/utils/url.py (+3/-2)
Reviewer Review Type Date Requested Status
Lee Trager (community) Approve
MAAS Lander Needs Fixing
Review via email: mp+333099@code.launchpad.net

Commit message

LP: #1418044 - Improve heuristic for determining best MAAS URL if it is not specifically configured.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b better-default-maas-url--bug-1418044 lp:~mpontillo/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/618/console
COMMIT: 25861b7702b4a6a29f061ac0994a6f0f4d78703d

review: Needs Fixing
Revision history for this message
Lee Trager (ltrager) wrote :

LGTM!

review: Approve
c01a722... by Mike Pontillo

Fix tests.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/compose_preseed.py b/src/maasserver/compose_preseed.py
2index 672a981..e15b3b2 100644
3--- a/src/maasserver/compose_preseed.py
4+++ b/src/maasserver/compose_preseed.py
5@@ -31,7 +31,7 @@ import yaml
6 RSYSLOG_PORT = 514
7
8
9-def get_apt_proxy(rack_controller=None):
10+def get_apt_proxy(rack_controller=None, default_region_ip=None):
11 """Return the APT proxy for the `rack_controller`."""
12 if Config.objects.get_config("enable_http_proxy"):
13 http_proxy = Config.objects.get_config("http_proxy")
14@@ -42,7 +42,8 @@ def get_apt_proxy(rack_controller=None):
15 return http_proxy
16 else:
17 return compose_URL(
18- "http://:8000/", get_maas_facing_server_host(rack_controller))
19+ "http://:8000/", get_maas_facing_server_host(
20+ rack_controller, default_region_ip=default_region_ip))
21 else:
22 return None
23
24@@ -55,11 +56,12 @@ def make_clean_repo_name(repo):
25 return repo_name.strip().replace(' ', '_').lower()
26
27
28-def get_archive_config(node, preserve_sources=False):
29+def get_archive_config(node, preserve_sources=False, default_region_ip=None):
30 arch = node.split_arch()[0]
31 archive = PackageRepository.objects.get_default_archive(arch)
32 repositories = PackageRepository.objects.get_additional_repositories(arch)
33- apt_proxy = get_apt_proxy(node.get_boot_rack_controller())
34+ apt_proxy = get_apt_proxy(
35+ node.get_boot_rack_controller(), default_region_ip=default_region_ip)
36
37 # Process the default Ubuntu Archives or Mirror.
38 archives = {}
39@@ -147,15 +149,15 @@ def get_archive_config(node, preserve_sources=False):
40 return archives
41
42
43-def get_cloud_init_reporting(node, token, base_url):
44+def get_cloud_init_reporting(node, token, base_url, default_region_ip=None):
45 """Return the cloud-init metadata to enable reporting"""
46 return {
47 'reporting': {
48 'maas': {
49 'type': 'webhook',
50 'endpoint': absolute_reverse(
51- 'metadata-status', args=[node.system_id],
52- base_url=base_url),
53+ 'metadata-status', default_region_ip=default_region_ip,
54+ args=[node.system_id], base_url=base_url),
55 'consumer_key': token.consumer.key,
56 'token_key': token.key,
57 'token_secret': token.secret,
58@@ -164,11 +166,11 @@ def get_cloud_init_reporting(node, token, base_url):
59 }
60
61
62-def get_rsyslog_host_port(node):
63+def get_rsyslog_host_port(node, default_region_ip=None):
64 """Return the rsyslog host and port to use."""
65- # TODO: In the future, we can make this configurable
66- return "%s:%d" % (get_maas_facing_server_host(
67- node.get_boot_rack_controller()), RSYSLOG_PORT)
68+ host = get_maas_facing_server_host(
69+ node.get_boot_rack_controller(), default_region_ip=default_region_ip)
70+ return "%s:%d" % (host, RSYSLOG_PORT)
71
72
73 def get_system_info():
74@@ -207,7 +209,8 @@ def get_system_info():
75 }
76
77
78-def compose_cloud_init_preseed(node, token, base_url=''):
79+def compose_cloud_init_preseed(
80+ node, token, base_url='', default_region_ip=None):
81 """Compose the preseed value for a node in any state but Commissioning.
82
83 Returns cloud-config that's preseeded to cloud-init via debconf (It only
84@@ -236,14 +239,20 @@ def compose_cloud_init_preseed(node, token, base_url=''):
85 # This will allow cloud-init to be configured with reporting for
86 # a node that has already been installed.
87 config.update(
88- get_cloud_init_reporting(node=node, token=token, base_url=base_url))
89+ get_cloud_init_reporting(
90+ node=node, token=token, base_url=base_url,
91+ default_region_ip=default_region_ip))
92 # Add the system configuration information.
93 config.update(get_system_info())
94- apt_proxy = get_apt_proxy(node.get_boot_rack_controller())
95+ apt_proxy = get_apt_proxy(
96+ node.get_boot_rack_controller(), default_region_ip=default_region_ip)
97 if apt_proxy:
98 config['apt_proxy'] = apt_proxy
99 # Add APT configuration for new cloud-init (>= 0.7.7-17)
100- config.update(get_archive_config(node=node, preserve_sources=False))
101+ config.update(
102+ get_archive_config(
103+ node=node, preserve_sources=False,
104+ default_region_ip=default_region_ip))
105
106 local_config_yaml = yaml.safe_dump(config)
107 # this is debconf escaping
108@@ -254,7 +263,8 @@ def compose_cloud_init_preseed(node, token, base_url=''):
109 preseed_items = [
110 ('datasources', 'multiselect', 'MAAS'),
111 ('maas-metadata-url', 'string', absolute_reverse(
112- 'metadata', base_url=base_url)),
113+ 'metadata', default_region_ip=default_region_ip,
114+ base_url=base_url)),
115 ('maas-metadata-credentials', 'string', credentials),
116 ('local-cloud-config', 'string', local_config)
117 ]
118@@ -268,28 +278,34 @@ def compose_cloud_init_preseed(node, token, base_url=''):
119 for item_name, item_type, item_value in preseed_items)
120
121
122-def compose_commissioning_preseed(node, token, base_url=''):
123+def compose_commissioning_preseed(
124+ node, token, base_url='', default_region_ip=None):
125 """Compose the preseed value for a Commissioning node."""
126- metadata_url = absolute_reverse('metadata', base_url=base_url)
127+ metadata_url = absolute_reverse(
128+ 'metadata', default_region_ip=default_region_ip, base_url=base_url)
129 if node.status == NODE_STATUS.DISK_ERASING:
130 poweroff_timeout = timedelta(days=7).total_seconds() # 1 week
131 else:
132 poweroff_timeout = timedelta(hours=1).total_seconds() # 1 hour
133 return _compose_cloud_init_preseed(
134 node, token, metadata_url, base_url=base_url,
135- poweroff_timeout=int(poweroff_timeout))
136+ poweroff_timeout=int(poweroff_timeout),
137+ default_region_ip=default_region_ip)
138
139
140-def compose_curtin_preseed(node, token, base_url=''):
141+def compose_curtin_preseed(node, token, base_url='', default_region_ip=None):
142 """Compose the preseed value for a node being installed with curtin."""
143- metadata_url = absolute_reverse('curtin-metadata', base_url=base_url)
144+ metadata_url = absolute_reverse(
145+ 'curtin-metadata', default_region_ip=default_region_ip,
146+ base_url=base_url)
147 return _compose_cloud_init_preseed(
148- node, token, metadata_url, base_url=base_url)
149+ node, token, metadata_url, base_url=base_url,
150+ default_region_ip=default_region_ip)
151
152
153 def _compose_cloud_init_preseed(
154 node, token, metadata_url, base_url, poweroff_timeout=3600,
155- reboot_timeout=1800):
156+ reboot_timeout=1800, default_region_ip=None):
157 cloud_config = {
158 'datasource': {
159 'MAAS': {
160@@ -302,7 +318,8 @@ def _compose_cloud_init_preseed(
161 # This configure rsyslog for the ephemeral environment
162 'rsyslog': {
163 'remotes': {
164- 'maas': get_rsyslog_host_port(node),
165+ 'maas': get_rsyslog_host_port(
166+ node, default_region_ip=default_region_ip),
167 }
168 },
169 # The ephemeral environment doesn't have a domain search path set which
170@@ -315,14 +332,19 @@ def _compose_cloud_init_preseed(
171 }
172 # This configures reporting for the ephemeral environment
173 cloud_config.update(
174- get_cloud_init_reporting(node=node, token=token, base_url=base_url))
175+ get_cloud_init_reporting(
176+ node=node, token=token, base_url=base_url,
177+ default_region_ip=default_region_ip))
178 # Add the system configuration information.
179 cloud_config.update(get_system_info())
180- apt_proxy = get_apt_proxy(node.get_boot_rack_controller())
181+ apt_proxy = get_apt_proxy(
182+ node.get_boot_rack_controller(), default_region_ip=default_region_ip)
183 if apt_proxy:
184 cloud_config['apt_proxy'] = apt_proxy
185 # Add APT configuration for new cloud-init (>= 0.7.7-17)
186- cloud_config.update(get_archive_config(node=node, preserve_sources=False))
187+ cloud_config.update(get_archive_config(
188+ node=node, preserve_sources=False,
189+ default_region_ip=default_region_ip))
190
191 enable_ssh = (node.status in {
192 NODE_STATUS.COMMISSIONING,
193@@ -352,14 +374,17 @@ def _compose_cloud_init_preseed(
194 return "#cloud-config\n%s" % yaml.safe_dump(cloud_config)
195
196
197-def _get_metadata_url(preseed_type, base_url):
198+def _get_metadata_url(preseed_type, base_url, default_region_ip=None):
199 if preseed_type == PRESEED_TYPE.CURTIN:
200- return absolute_reverse('curtin-metadata', base_url=base_url)
201+ return absolute_reverse(
202+ 'curtin-metadata', default_region_ip=default_region_ip,
203+ base_url=base_url)
204 else:
205- return absolute_reverse('metadata', base_url=base_url)
206+ return absolute_reverse(
207+ 'metadata', default_region_ip=default_region_ip, base_url=base_url)
208
209
210-def compose_preseed(preseed_type, node):
211+def compose_preseed(preseed_type, node, default_region_ip=None):
212 """Put together preseed data for `node`.
213
214 This produces preseed data for the node in different formats depending
215@@ -369,6 +394,8 @@ def compose_preseed(preseed_type, node):
216 :type preseed_type: string
217 :param node: The node to compose preseed data for.
218 :type node: Node
219+ :param default_region_ip: The default IP address to use for the region
220+ controller (for example, when constructing URLs).
221 :return: Preseed data containing the information the node needs in order
222 to access the metadata service: its URL and auth token.
223 """
224@@ -380,9 +407,11 @@ def compose_preseed(preseed_type, node):
225 base_url = rack_controller.url
226
227 if preseed_type == PRESEED_TYPE.COMMISSIONING:
228- return compose_commissioning_preseed(node, token, base_url)
229+ return compose_commissioning_preseed(
230+ node, token, base_url, default_region_ip=default_region_ip)
231 else:
232- metadata_url = _get_metadata_url(preseed_type, base_url)
233+ metadata_url = _get_metadata_url(
234+ preseed_type, base_url, default_region_ip=default_region_ip)
235
236 try:
237 return get_preseed_data(preseed_type, node, token, metadata_url)
238@@ -410,6 +439,8 @@ def compose_preseed(preseed_type, node):
239
240 # There is no OS-specific preseed data.
241 if preseed_type == PRESEED_TYPE.CURTIN:
242- return compose_curtin_preseed(node, token, base_url)
243+ return compose_curtin_preseed(
244+ node, token, base_url, default_region_ip=default_region_ip)
245 else:
246- return compose_cloud_init_preseed(node, token, base_url)
247+ return compose_cloud_init_preseed(
248+ node, token, base_url, default_region_ip=default_region_ip)
249diff --git a/src/maasserver/preseed.py b/src/maasserver/preseed.py
250index 3475dd0..f2ed29c 100644
251--- a/src/maasserver/preseed.py
252+++ b/src/maasserver/preseed.py
253@@ -95,7 +95,7 @@ OS_WITH_IPv6_SUPPORT = ['ubuntu']
254 CURTIN_INSTALL_LOG = "/tmp/install.log"
255
256
257-def get_enlist_preseed(rack_controller=None):
258+def get_enlist_preseed(rack_controller=None, default_region_ip=None):
259 """Return the enlistment preseed.
260
261 :param rack_controller: The rack controller used to generate the preseed.
262@@ -103,17 +103,19 @@ def get_enlist_preseed(rack_controller=None):
263 :rtype: unicode.
264 """
265 return render_enlistment_preseed(
266- PRESEED_TYPE.ENLIST, rack_controller=rack_controller)
267+ PRESEED_TYPE.ENLIST, rack_controller=rack_controller,
268+ default_region_ip=default_region_ip)
269
270
271-def get_enlist_userdata(rack_controller=None):
272+def get_enlist_userdata(rack_controller=None, default_region_ip=None):
273 """Return the enlistment preseed.
274
275 :param rack_controller: The rack controller used to generate the preseed.
276 :return: The rendered enlistment user-data string.
277 :rtype: unicode.
278 """
279- http_proxy = get_apt_proxy(rack_controller=rack_controller)
280+ http_proxy = get_apt_proxy(
281+ rack_controller=rack_controller, default_region_ip=default_region_ip)
282 enlist_userdata = render_enlistment_preseed(
283 USERDATA_TYPE.ENLIST, rack_controller=rack_controller)
284 config = get_system_info()
285@@ -340,7 +342,7 @@ def compose_curtin_verbose_preseed():
286 return []
287
288
289-def get_curtin_yaml_config(node):
290+def get_curtin_yaml_config(node, default_region_ip=None):
291 """Return the curtin configration for the node."""
292 main_config = get_curtin_config(node)
293 cloud_config = compose_curtin_cloud_config(node)
294@@ -401,7 +403,7 @@ def get_curtin_merged_config(node):
295 return config
296
297
298-def get_curtin_userdata(node):
299+def get_curtin_userdata(node, default_region_ip=None):
300 """Return the curtin user-data.
301
302 :param node: The node for which to generate the user-data.
303@@ -411,7 +413,7 @@ def get_curtin_userdata(node):
304 # Pack the curtin and the configuration into a script to execute on the
305 # deploying node.
306 return pack_install(
307- configs=get_curtin_yaml_config(node),
308+ configs=get_curtin_yaml_config(node, default_region_ip),
309 args=[get_curtin_installer_url(node)])
310
311
312@@ -473,7 +475,7 @@ def get_curtin_installer_url(node):
313 return url_prepend + url
314
315
316-def get_curtin_config(node):
317+def get_curtin_config(node, default_region_ip=None):
318 """Return the curtin configuration to be used by curtin.pack_install.
319
320 :param node: The node for which to generate the configuration.
321@@ -485,11 +487,16 @@ def get_curtin_config(node):
322 node, USERDATA_TYPE.CURTIN, osystem, series)
323 rack_controller = node.get_boot_rack_controller()
324 context = get_preseed_context(
325- osystem, series, rack_controller=rack_controller)
326+ osystem, series, rack_controller=rack_controller,
327+ default_region_ip=default_region_ip)
328 context.update(
329 get_node_preseed_context(
330- node, osystem, series, rack_controller=rack_controller))
331- context.update(get_curtin_context(node, rack_controller=rack_controller))
332+ node, osystem, series, rack_controller=rack_controller,
333+ default_region_ip=default_region_ip))
334+ context.update(
335+ get_curtin_context(
336+ node, rack_controller=rack_controller,
337+ default_region_ip=default_region_ip))
338 deprecated_context_variables = [
339 'main_archive_hostname', 'main_archive_directory',
340 'ports_archive_hostname', 'ports_archive_directory',
341@@ -534,7 +541,7 @@ def get_curtin_config(node):
342 return yaml.safe_dump(config)
343
344
345-def get_curtin_context(node, rack_controller=None):
346+def get_curtin_context(node, rack_controller=None, default_region_ip=None):
347 """Return the curtin-specific context dictionary to be used to render
348 user-data templates.
349
350@@ -546,7 +553,8 @@ def get_curtin_context(node, rack_controller=None):
351 rack_controller = node.get_boot_rack_controller()
352 base_url = rack_controller.url
353 return {
354- 'curtin_preseed': compose_cloud_init_preseed(node, token, base_url)
355+ 'curtin_preseed': compose_cloud_init_preseed(
356+ node, token, base_url, default_region_ip=default_region_ip)
357 }
358
359
360@@ -567,7 +575,7 @@ def get_preseed_type_for(node):
361
362
363 @typed
364-def get_preseed(node) -> bytes:
365+def get_preseed(node, default_region_ip=None) -> bytes:
366 """Return the preseed for a given node. Depending on the node's
367 status this will be a commissioning preseed (if the node is
368 commissioning or disk erasing) or an install preseed (normal
369@@ -582,11 +590,13 @@ def get_preseed(node) -> bytes:
370 return render_preseed(
371 node, PRESEED_TYPE.COMMISSIONING,
372 osystem=Config.objects.get_config('commissioning_osystem'),
373- release=Config.objects.get_config('commissioning_distro_series'))
374+ release=Config.objects.get_config('commissioning_distro_series'),
375+ default_region_ip=default_region_ip)
376 else:
377 return render_preseed(
378 node, get_preseed_type_for(node),
379- osystem=node.get_osystem(), release=node.get_distro_series())
380+ osystem=node.get_osystem(), release=node.get_distro_series(),
381+ default_region_ip=default_region_ip)
382
383
384 UBUNTU_NAME = UbuntuOS().name
385@@ -769,7 +779,8 @@ def get_netloc_and_path(url):
386 return parsed_url.netloc, parsed_url.path.lstrip("/")
387
388
389-def get_preseed_context(osystem='', release='', rack_controller=None):
390+def get_preseed_context(
391+ osystem='', release='', rack_controller=None, default_region_ip=None):
392 """Return the node-independent context dictionary to be used to render
393 preseed templates.
394
395@@ -779,7 +790,8 @@ def get_preseed_context(osystem='', release='', rack_controller=None):
396 :return: The context dictionary.
397 :rtype: dict.
398 """
399- server_host = get_maas_facing_server_host(rack_controller=rack_controller)
400+ server_host = get_maas_facing_server_host(
401+ rack_controller=rack_controller, default_region_ip=default_region_ip)
402 if rack_controller is None:
403 base_url = None
404 else:
405@@ -789,14 +801,18 @@ def get_preseed_context(osystem='', release='', rack_controller=None):
406 'osystem': osystem,
407 'release': release,
408 'server_host': server_host,
409- 'server_url': absolute_reverse('machines_handler', base_url=base_url),
410+ 'server_url': absolute_reverse(
411+ 'machines_handler', default_region_ip=default_region_ip,
412+ base_url=base_url),
413 'syslog_host_port': '%s:%d' % (server_host, RSYSLOG_PORT),
414- 'metadata_enlist_url': absolute_reverse('enlist', base_url=base_url),
415+ 'metadata_enlist_url': absolute_reverse(
416+ 'enlist', default_region_ip=default_region_ip, base_url=base_url),
417 }
418
419
420 def get_node_preseed_context(
421- node, osystem='', release='', rack_controller=None):
422+ node, osystem='', release='', rack_controller=None,
423+ default_region_ip=None):
424 """Return the node-dependent context dictionary to be used to render
425 preseed templates.
426
427@@ -811,8 +827,8 @@ def get_node_preseed_context(
428 # Create the url and the url-data (POST parameters) used to turn off
429 # PXE booting once the install of the node is finished.
430 node_disable_pxe_url = absolute_reverse(
431- 'metadata-node-by-id', args=['latest', node.system_id],
432- base_url=rack_controller.url)
433+ 'metadata-node-by-id', default_region_ip=default_region_ip,
434+ args=['latest', node.system_id], base_url=rack_controller.url)
435 node_disable_pxe_data = urlencode({'op': 'netboot_off'})
436 driver = get_third_party_driver(node)
437 return {
438@@ -821,7 +837,9 @@ def get_node_preseed_context(
439 'driver': driver,
440 'driver_package': driver.get('package', ''),
441 'node': node,
442- 'preseed_data': compose_preseed(get_preseed_type_for(node), node),
443+ 'preseed_data': compose_preseed(
444+ get_preseed_type_for(node), node,
445+ default_region_ip=default_region_ip),
446 'node_disable_pxe_url': node_disable_pxe_url,
447 'node_disable_pxe_data': node_disable_pxe_data,
448 'license_key': node.get_effective_license_key(),
449@@ -853,7 +871,8 @@ def get_node_deprecated_preseed_context():
450
451
452 def render_enlistment_preseed(
453- prefix, osystem='', release='', rack_controller=None):
454+ prefix, osystem='', release='', rack_controller=None,
455+ default_region_ip=None):
456 """Return the enlistment preseed.
457
458 :param prefix: See `get_preseed_filenames`.
459@@ -865,14 +884,16 @@ def render_enlistment_preseed(
460 """
461 template = load_preseed_template(None, prefix, osystem, release)
462 context = get_preseed_context(
463- osystem, release, rack_controller=rack_controller)
464+ osystem, release, rack_controller=rack_controller,
465+ default_region_ip=default_region_ip)
466 # Render the snippets in the main template.
467 snippets = get_snippet_context()
468 snippets.update(context)
469 return template.substitute(**snippets).encode("utf-8")
470
471
472-def render_preseed(node, prefix, osystem='', release=''):
473+def render_preseed(
474+ node, prefix, osystem='', release='', default_region_ip=None):
475 """Return the preseed for the given node.
476
477 :param node: See `get_preseed_filenames`.
478@@ -885,17 +906,22 @@ def render_preseed(node, prefix, osystem='', release=''):
479 template = load_preseed_template(node, prefix, osystem, release)
480 rack_controller = node.get_boot_rack_controller()
481 context = get_preseed_context(
482- osystem, release, rack_controller=rack_controller)
483+ osystem, release, rack_controller=rack_controller,
484+ default_region_ip=default_region_ip)
485 context.update(
486 get_node_preseed_context(
487- node, osystem, release, rack_controller=rack_controller))
488+ node, osystem, release, rack_controller=rack_controller,
489+ default_region_ip=default_region_ip))
490 return template.substitute(**context).encode("utf-8")
491
492
493-def compose_enlistment_preseed_url(rack_controller=None):
494+def compose_enlistment_preseed_url(
495+ rack_controller=None, default_region_ip=None):
496 """Compose enlistment preseed URL.
497
498 :param rack_controller: The rack controller used to generate the preseed.
499+ :param default_region_ip: The preferred IP address this region should
500+ communicate on.
501 """
502 # Always uses the latest version of the metadata API.
503 base_url = (
504@@ -904,14 +930,15 @@ def compose_enlistment_preseed_url(rack_controller=None):
505 else None)
506 version = 'latest'
507 return absolute_reverse(
508- 'metadata-enlist-preseed', args=[version],
509- query={'op': 'get_enlist_preseed'}, base_url=base_url)
510+ 'metadata-enlist-preseed', default_region_ip=default_region_ip,
511+ args=[version], query={'op': 'get_enlist_preseed'}, base_url=base_url)
512
513
514-def compose_preseed_url(node, rack_controller):
515+def compose_preseed_url(node, rack_controller, default_region_ip=None):
516 """Compose a metadata URL for `node`'s preseed data."""
517 # Always uses the latest version of the metadata API.
518 version = 'latest'
519 return absolute_reverse(
520- 'metadata-node-by-id', args=[version, node.system_id],
521- query={'op': 'get_preseed'}, base_url=rack_controller.url)
522+ 'metadata-node-by-id', default_region_ip=default_region_ip,
523+ args=[version, node.system_id], query={'op': 'get_preseed'},
524+ base_url=rack_controller.url)
525diff --git a/src/maasserver/rpc/boot.py b/src/maasserver/rpc/boot.py
526index 9d27d7b..e4c0acd 100644
527--- a/src/maasserver/rpc/boot.py
528+++ b/src/maasserver/rpc/boot.py
529@@ -43,6 +43,7 @@ from maasserver.utils.orm import (
530 from maasserver.utils.osystems import validate_hwe_kernel
531 from provisioningserver.events import EVENT_TYPES
532 from provisioningserver.rpc.exceptions import BootConfigNoResponse
533+from provisioningserver.utils.network import get_source_address
534 from provisioningserver.utils.twisted import synchronous
535
536
537@@ -184,6 +185,9 @@ def get_config(
538 Raises BootConfigNoResponse when booting machine should fail to next file.
539 """
540 rack_controller = RackController.objects.get(system_id=system_id)
541+ region_ip = None
542+ if remote_ip is not None:
543+ region_ip = get_source_address(remote_ip)
544 machine = get_node_from_mac_string(mac)
545
546 # Fail with no response early so no extra work is performed.
547@@ -232,7 +236,8 @@ def get_config(
548 machine.boot_interface.save()
549
550 arch, subarch = machine.split_arch()
551- preseed_url = compose_preseed_url(machine, rack_controller)
552+ preseed_url = compose_preseed_url(
553+ machine, rack_controller, default_region_ip=region_ip)
554 hostname = machine.hostname
555 domain = machine.domain.name
556 purpose = machine.get_boot_purpose()
557@@ -305,7 +310,8 @@ def get_config(
558 extra_kernel_opts)
559 else:
560 purpose = "commissioning" # enlistment
561- preseed_url = compose_enlistment_preseed_url(rack_controller)
562+ preseed_url = compose_enlistment_preseed_url(
563+ rack_controller, default_region_ip=region_ip)
564 hostname = 'maas-enlist'
565 domain = 'local'
566 osystem = Config.objects.get_config('commissioning_osystem')
567@@ -358,7 +364,7 @@ def get_config(
568
569 # Get the service address to the region for that given rack controller.
570 server_host = get_maas_facing_server_host(
571- rack_controller=rack_controller)
572+ rack_controller=rack_controller, default_region_ip=region_ip)
573
574 kernel, initrd, boot_dtb = get_boot_filenames(
575 arch, subarch, osystem, series)
576diff --git a/src/maasserver/rpc/tests/test_boot.py b/src/maasserver/rpc/tests/test_boot.py
577index 04cc5d4..84dfcf8 100644
578--- a/src/maasserver/rpc/tests/test_boot.py
579+++ b/src/maasserver/rpc/tests/test_boot.py
580@@ -43,6 +43,7 @@ from maastesting.matchers import (
581 )
582 from netaddr import IPNetwork
583 from provisioningserver.rpc.exceptions import BootConfigNoResponse
584+from provisioningserver.utils.network import get_source_address
585 from testtools.matchers import (
586 ContainsAll,
587 StartsWith,
588@@ -276,7 +277,8 @@ class TestGetConfig(MAASServerTestCase):
589 observed_config = get_config(
590 rack_controller.system_id, local_ip, remote_ip)
591 self.assertEqual(
592- compose_enlistment_preseed_url(),
593+ compose_enlistment_preseed_url(
594+ default_region_ip=get_source_address(remote_ip)),
595 observed_config["preseed_url"])
596
597 def test__enlistment_checks_default_min_hwe_kernel(self):
598@@ -306,15 +308,17 @@ class TestGetConfig(MAASServerTestCase):
599 self.assertEqual('generic', observed_config['subarch'])
600
601 def test__has_preseed_url_for_known_node(self):
602- rack_controller = factory.make_RackController()
603+ rack_controller = factory.make_RackController(url='')
604 local_ip = factory.make_ip_address()
605 remote_ip = factory.make_ip_address()
606 node = self.make_node(status=NODE_STATUS.DEPLOYING)
607 mac = node.get_boot_interface().mac_address
608+ self.patch(boot_module, 'get_source_address').return_value = local_ip
609 observed_config = get_config(
610 rack_controller.system_id, local_ip, remote_ip, mac=mac)
611 self.assertEqual(
612- compose_preseed_url(node, rack_controller),
613+ compose_preseed_url(
614+ node, rack_controller, default_region_ip=local_ip),
615 observed_config["preseed_url"])
616
617 def test_preseed_url_for_known_node_uses_rack_url(self):
618diff --git a/src/maasserver/server_address.py b/src/maasserver/server_address.py
619index 855b90d..b543556 100644
620--- a/src/maasserver/server_address.py
621+++ b/src/maasserver/server_address.py
622@@ -19,15 +19,19 @@ from provisioningserver.utils.env import get_maas_id
623 from provisioningserver.utils.network import resolve_hostname
624
625
626-def get_maas_facing_server_host(rack_controller=None):
627+def get_maas_facing_server_host(rack_controller=None, default_region_ip=None):
628 """Return configured MAAS server hostname, for use by nodes or workers.
629
630 :param rack_controller: The `RackController` from the point of view of
631 which the server host should be computed.
632+ :param default_region_ip: The default source IP address to be used, if a
633+ specific URL is not defined.
634 :return: Hostname or IP address, as configured in the MAAS URL config
635 setting or as configured on rack_controller.url.
636 """
637 if rack_controller is None or not rack_controller.url:
638+ if default_region_ip is not None:
639+ return default_region_ip
640 with RegionConfiguration.open() as config:
641 maas_url = config.maas_url
642 else:
643diff --git a/src/maasserver/tests/test_compose_preseed.py b/src/maasserver/tests/test_compose_preseed.py
644index 7d868e0..62a9a39 100644
645--- a/src/maasserver/tests/test_compose_preseed.py
646+++ b/src/maasserver/tests/test_compose_preseed.py
647@@ -48,41 +48,92 @@ class TestAptProxy(MAASServerTestCase):
648
649 scenarios = (
650 ("ipv6", dict(
651+ default_region_ip=None,
652 rack='2001:db8::1',
653 result='http://[2001:db8::1]:8000/',
654 enable=True,
655 use_peer_proxy=False,
656 http_proxy='')),
657 ("ipv4", dict(
658+ default_region_ip=None,
659 rack='10.0.1.1',
660 result='http://10.0.1.1:8000/',
661 enable=True,
662 use_peer_proxy=False,
663 http_proxy='')),
664 ("builtin", dict(
665+ default_region_ip=None,
666 rack='region.example.com',
667 result='http://region.example.com:8000/',
668 enable=True,
669 use_peer_proxy=False,
670 http_proxy='')),
671 ("external", dict(
672+ default_region_ip=None,
673 rack='region.example.com',
674 result='http://proxy.example.com:111/',
675 enable=True,
676 use_peer_proxy=False,
677 http_proxy='http://proxy.example.com:111/')),
678 ("peer-proxy", dict(
679+ default_region_ip=None,
680 rack='region.example.com',
681 result='http://region.example.com:8000/',
682 enable=True,
683 use_peer_proxy=True,
684 http_proxy='http://proxy.example.com:111/')),
685 ("disabled", dict(
686+ default_region_ip=None,
687 rack='example.com',
688 result=None,
689 enable=False,
690 use_peer_proxy=False,
691 http_proxy='')),
692+ # If a default IP address for the region is passed in and the rack's
693+ # URL is empty, the default IP address that was provided should be
694+ # preferred.
695+ ("ipv6_default", dict(
696+ default_region_ip='2001:db8::2',
697+ rack='',
698+ result='http://[2001:db8::2]:8000/',
699+ enable=True,
700+ use_peer_proxy=False,
701+ http_proxy='')),
702+ ("ipv4_default", dict(
703+ default_region_ip='10.0.1.2',
704+ rack='',
705+ result='http://10.0.1.2:8000/',
706+ enable=True,
707+ use_peer_proxy=False,
708+ http_proxy='')),
709+ ("builtin_default", dict(
710+ default_region_ip='region.example.com',
711+ rack='',
712+ result='http://region.example.com:8000/',
713+ enable=True,
714+ use_peer_proxy=False,
715+ http_proxy='')),
716+ ("external_default", dict(
717+ default_region_ip='10.0.0.1',
718+ rack='',
719+ result='http://proxy.example.com:111/',
720+ enable=True,
721+ use_peer_proxy=False,
722+ http_proxy='http://proxy.example.com:111/')),
723+ ("peer-proxy_default", dict(
724+ default_region_ip='region2.example.com',
725+ rack='',
726+ result='http://region2.example.com:8000/',
727+ enable=True,
728+ use_peer_proxy=True,
729+ http_proxy='http://proxy.example.com:111/')),
730+ ("disabled_default", dict(
731+ default_region_ip='10.0.0.1',
732+ rack='',
733+ result=None,
734+ enable=False,
735+ use_peer_proxy=False,
736+ http_proxy='')),
737 )
738
739 def test__returns_correct_url(self):
740@@ -94,14 +145,17 @@ class TestAptProxy(MAASServerTestCase):
741 # Force the server host to be our test data.
742 self.patch(
743 cp_module,
744- 'get_maas_facing_server_host').return_value = self.rack
745+ 'get_maas_facing_server_host').return_value = (
746+ self.rack if self.rack else self.default_region_ip)
747 # Now setup the configuration and arguments, and see what we get back.
748 node = factory.make_Node(
749 interface=True, status=NODE_STATUS.COMMISSIONING)
750 Config.objects.set_config("enable_http_proxy", self.enable)
751 Config.objects.set_config("http_proxy", self.http_proxy)
752 Config.objects.set_config("use_peer_proxy", self.use_peer_proxy)
753- actual = get_apt_proxy(node.get_boot_rack_controller())
754+ actual = get_apt_proxy(
755+ node.get_boot_rack_controller(),
756+ default_region_ip=self.default_region_ip)
757 self.assertEqual(self.result, actual)
758
759
760@@ -241,20 +295,45 @@ class TestComposePreseed(MAASServerTestCase):
761
762 def test_compose_preseed_for_commissioning_includes_metadata_status_url(
763 self):
764- rack_controller = factory.make_RackController()
765+ rack_controller = factory.make_RackController(url='')
766 node = factory.make_Node(
767 interface=True, status=NODE_STATUS.COMMISSIONING)
768 nic = node.get_boot_interface()
769 nic.vlan.dhcp_on = True
770 nic.vlan.primary_rack = rack_controller
771 nic.vlan.save()
772+ region_ip = factory.make_ip_address()
773 preseed = yaml.safe_load(
774- compose_preseed(PRESEED_TYPE.COMMISSIONING, node))
775+ compose_preseed(
776+ PRESEED_TYPE.COMMISSIONING, node, default_region_ip=region_ip))
777+ self.assertEqual(
778+ absolute_reverse('metadata', default_region_ip=region_ip),
779+ preseed['datasource']['MAAS']['metadata_url'])
780+ self.assertEqual(
781+ absolute_reverse(
782+ 'metadata-status', default_region_ip=region_ip,
783+ args=[node.system_id]),
784+ preseed['reporting']['maas']['endpoint'])
785+
786+ def test_compose_preseed_uses_default_region_ip(self):
787+ rack_controller = factory.make_RackController(url='')
788+ node = factory.make_Node(
789+ interface=True, status=NODE_STATUS.COMMISSIONING)
790+ nic = node.get_boot_interface()
791+ nic.vlan.dhcp_on = True
792+ nic.vlan.primary_rack = rack_controller
793+ nic.vlan.save()
794+ preseed = yaml.safe_load(
795+ compose_preseed(
796+ PRESEED_TYPE.COMMISSIONING, node,
797+ default_region_ip='10.0.0.1'))
798 self.assertEqual(
799- absolute_reverse('metadata'),
800+ absolute_reverse('metadata', default_region_ip='10.0.0.1'),
801 preseed['datasource']['MAAS']['metadata_url'])
802 self.assertEqual(
803- absolute_reverse('metadata-status', args=[node.system_id]),
804+ absolute_reverse(
805+ 'metadata-status', default_region_ip='10.0.0.1',
806+ args=[node.system_id]),
807 preseed['reporting']['maas']['endpoint'])
808
809 def test_compose_preseed_for_rescue_mode_does_not_include_poweroff(self):
810@@ -369,7 +448,7 @@ class TestComposePreseed(MAASServerTestCase):
811 self.assertEqual(token.secret, reporting_dict['token_secret'])
812
813 def test_compose_preseed_with_curtin_installer(self):
814- rack_controller = factory.make_RackController()
815+ rack_controller = factory.make_RackController(url='')
816 node = factory.make_Node(
817 interface=True, status=NODE_STATUS.DEPLOYING)
818 nic = node.get_boot_interface()
819@@ -377,9 +456,12 @@ class TestComposePreseed(MAASServerTestCase):
820 nic.vlan.primary_rack = rack_controller
821 nic.vlan.save()
822 self.useFixture(RunningClusterRPCFixture())
823- apt_proxy = get_apt_proxy(node.get_boot_rack_controller())
824+ region_ip = factory.make_ip_address()
825+ expected_apt_proxy = get_apt_proxy(
826+ node.get_boot_rack_controller(), default_region_ip=region_ip)
827 preseed = yaml.safe_load(
828- compose_preseed(PRESEED_TYPE.CURTIN, node))
829+ compose_preseed(
830+ PRESEED_TYPE.CURTIN, node, default_region_ip=region_ip))
831
832 self.assertIn('datasource', preseed)
833 self.assertIn('MAAS', preseed['datasource'])
834@@ -395,11 +477,11 @@ class TestComposePreseed(MAASServerTestCase):
835 'condition': 'test ! -e /tmp/block-reboot',
836 }, preseed['power_state'])
837 self.assertEqual(
838- absolute_reverse('curtin-metadata'),
839+ absolute_reverse('curtin-metadata', default_region_ip=region_ip),
840 preseed['datasource']['MAAS']['metadata_url'])
841- self.assertEqual(apt_proxy, preseed['apt_proxy'])
842+ self.assertEqual(expected_apt_proxy, preseed['apt_proxy'])
843 self.assertSystemInfo(preseed)
844- self.assertAptConfig(preseed, apt_proxy)
845+ self.assertAptConfig(preseed, expected_apt_proxy)
846
847 def test_compose_preseed_with_curtin_installer_skips_apt_proxy(self):
848 # Disable boot source cache signals.
849@@ -428,7 +510,7 @@ class TestComposePreseed(MAASServerTestCase):
850 compose_preseed_mock = self.patch(osystem, 'compose_preseed')
851 compose_preseed_mock.side_effect = compose_preseed_orig
852
853- rack_controller = factory.make_RackController()
854+ rack_controller = factory.make_RackController(url='')
855 node = factory.make_Node(
856 interface=True, osystem=os_name, status=NODE_STATUS.READY)
857 nic = node.get_boot_interface()
858@@ -437,15 +519,18 @@ class TestComposePreseed(MAASServerTestCase):
859 nic.vlan.save()
860 self.useFixture(RunningClusterRPCFixture())
861 token = NodeKey.objects.get_token_for_node(node)
862- url = absolute_reverse('curtin-metadata')
863- compose_preseed(PRESEED_TYPE.CURTIN, node)
864+ region_ip = factory.make_ip_address()
865+ expected_url = absolute_reverse(
866+ 'curtin-metadata', default_region_ip=region_ip)
867+ compose_preseed(
868+ PRESEED_TYPE.CURTIN, node, default_region_ip=region_ip)
869 self.assertThat(
870 compose_preseed_mock,
871 MockCalledOnceWith(
872 PRESEED_TYPE.CURTIN,
873 (node.system_id, node.hostname),
874 (token.consumer.key, token.key, token.secret),
875- url))
876+ expected_url))
877
878 def test_compose_preseed_propagates_NoSuchOperatingSystem(self):
879 # If the cluster controller replies that the node's OS is not known to
880diff --git a/src/maasserver/tests/test_preseed.py b/src/maasserver/tests/test_preseed.py
881index d78b267..6f5e7d6 100644
882--- a/src/maasserver/tests/test_preseed.py
883+++ b/src/maasserver/tests/test_preseed.py
884@@ -1834,9 +1834,11 @@ class TestPreseedURLs(
885 """Tests for functions that return preseed URLs."""
886
887 def test_compose_enlistment_preseed_url_links_to_enlistment_preseed(self):
888- response = self.client.get(compose_enlistment_preseed_url())
889+ response = self.client.get(compose_enlistment_preseed_url(
890+ default_region_ip="127.0.0.1"))
891 self.assertEqual(
892- (http.client.OK, get_enlist_preseed()),
893+ (http.client.OK, get_enlist_preseed(
894+ default_region_ip="127.0.0.1")),
895 (response.status_code, response.content))
896
897 def test_compose_enlistment_preseed_url_returns_absolute_link(self):
898@@ -1846,7 +1848,7 @@ class TestPreseedURLs(
899 self.assertThat(
900 compose_enlistment_preseed_url(), StartsWith(maas_url))
901
902- def test_compose_enlistment_preseed_url_returns_abs_link_wth_nodegrp(self):
903+ def test_compose_enlistment_preseed_url_returns_abs_link_wth_rack(self):
904 maas_url = factory.make_simple_http_url(path='')
905 self.useFixture(RegionConfigurationFixture(maas_url=maas_url))
906 rack_controller = factory.make_RackController(url=maas_url)
907@@ -1860,9 +1862,10 @@ class TestPreseedURLs(
908 primary_rack=self.rpc_rack_controller)
909 self.configure_get_boot_images_for_node(node, 'install')
910 response = self.client.get(
911- compose_preseed_url(node, self.rpc_rack_controller))
912+ compose_preseed_url(
913+ node, self.rpc_rack_controller, default_region_ip='127.0.0.1'))
914 self.assertEqual(
915- (http.client.OK, get_preseed(node)),
916+ (http.client.OK, get_preseed(node, default_region_ip='127.0.0.1')),
917 (response.status_code, response.content))
918
919 def test_compose_preseed_url_returns_absolute_link(self):
920diff --git a/src/maasserver/utils/__init__.py b/src/maasserver/utils/__init__.py
921index 5963280..1c58859 100644
922--- a/src/maasserver/utils/__init__.py
923+++ b/src/maasserver/utils/__init__.py
924@@ -27,6 +27,7 @@ from provisioningserver.config import (
925 ClusterConfiguration,
926 UUID_NOT_SET,
927 )
928+from provisioningserver.utils.url import compose_URL
929 from provisioningserver.utils.version import get_maas_version_user_agent
930
931
932@@ -39,7 +40,9 @@ def ignore_unused(*args):
933 """
934
935
936-def absolute_reverse(view_name, query=None, base_url=None, *args, **kwargs):
937+def absolute_reverse(
938+ view_name, default_region_ip=None, query=None, base_url=None,
939+ *args, **kwargs):
940 """Return the absolute URL (i.e. including the URL scheme specifier and
941 the network location of the MAAS server). Internally this method simply
942 calls Django's 'reverse' method and prefixes the result of that call with
943@@ -50,6 +53,8 @@ def absolute_reverse(view_name, query=None, base_url=None, *args, **kwargs):
944
945 :param view_name: Django's view function name/reference or URL pattern
946 name for which to compute the absolute URL.
947+ :param default_region_ip: The default source IP address that should be
948+ used for the region controller.
949 :param query: Optional query argument which will be passed down to
950 urllib.urlencode. The result of that call will be appended to the
951 resulting url.
952@@ -57,11 +62,12 @@ def absolute_reverse(view_name, query=None, base_url=None, *args, **kwargs):
953 configured MAAS URL will be used.
954 :param args: Positional arguments for Django's 'reverse' method.
955 :param kwargs: Named arguments for Django's 'reverse' method.
956-
957 """
958 if not base_url:
959 with RegionConfiguration.open() as config:
960 base_url = config.maas_url
961+ if default_region_ip is not None:
962+ base_url = compose_URL(base_url, default_region_ip)
963 url = urljoin(base_url, reverse(view_name, *args, **kwargs))
964 if query is not None:
965 url += '?%s' % urlencode(query, doseq=True)
966@@ -135,6 +141,11 @@ def get_maas_user_agent():
967 return user_agent
968
969
970+def get_remote_ip(request):
971+ """Returns the IP address of the host that initiated the request."""
972+ return request.META.get('REMOTE_ADDR')
973+
974+
975 def find_rack_controller(request):
976 """Find the rack controller whose managing the subnet that contains the
977 requester's address.
978@@ -144,10 +155,7 @@ def find_rack_controller(request):
979 """
980 # Circular imports.
981 from maasserver.models.subnet import Subnet
982- ip_address = request.META['REMOTE_ADDR']
983- if ip_address is None:
984- return None
985-
986+ ip_address = get_remote_ip(request)
987 subnet = Subnet.objects.get_best_subnet_for_ip(ip_address)
988 if subnet is None:
989 return None
990diff --git a/src/metadataserver/api.py b/src/metadataserver/api.py
991index 18153ea..1466454 100644
992--- a/src/metadataserver/api.py
993+++ b/src/metadataserver/api.py
994@@ -12,7 +12,7 @@ __all__ = [
995 'MetaDataHandler',
996 'UserDataHandler',
997 'VersionIndexHandler',
998- ]
999+]
1000
1001 import base64
1002 from datetime import datetime
1003@@ -70,7 +70,10 @@ from maasserver.preseed import (
1004 get_enlist_userdata,
1005 get_preseed,
1006 )
1007-from maasserver.utils import find_rack_controller
1008+from maasserver.utils import (
1009+ find_rack_controller,
1010+ get_remote_ip,
1011+)
1012 from maasserver.utils.orm import (
1013 get_one,
1014 is_retryable_failure,
1015@@ -98,6 +101,7 @@ from provisioningserver.events import (
1016 EVENT_TYPES,
1017 )
1018 from provisioningserver.logger import LegacyLogger
1019+from provisioningserver.utils.network import get_source_address
1020 import yaml
1021
1022
1023@@ -162,6 +166,15 @@ def get_queried_node(request, for_mac=None):
1024 return get_node_for_mac(for_mac)
1025
1026
1027+def get_default_region_ip(request):
1028+ """Returns the default reply address for the given HTTP request."""
1029+ remote_ip = get_remote_ip(request)
1030+ default_region_ip = None
1031+ if remote_ip is not None:
1032+ default_region_ip = get_source_address(remote_ip)
1033+ return default_region_ip
1034+
1035+
1036 def make_text_response(contents):
1037 """Create a response containing `contents` as plain text."""
1038 # XXX: Set a charset for text/plain. Django automatically encodes
1039@@ -841,7 +854,8 @@ class CurtinUserDataHandler(MetadataViewHandler):
1040 def read(self, request, version, mac=None):
1041 check_version(version)
1042 node = get_queried_node(request, for_mac=mac)
1043- user_data = get_curtin_userdata(node)
1044+ default_region_ip = get_default_region_ip(request)
1045+ user_data = get_curtin_userdata(node, default_region_ip)
1046 return HttpResponse(
1047 user_data,
1048 content_type='application/octet-stream')
1049@@ -1032,12 +1046,15 @@ class EnlistUserDataHandler(OperationsHandler):
1050 def read(self, request, version):
1051 check_version(version)
1052 rack_controller = find_rack_controller(request)
1053+ default_region_ip = get_default_region_ip(request)
1054 # XXX: Set a charset for text/plain. Django automatically encodes
1055 # non-binary content using DEFAULT_CHARSET (which is UTF-8 by default)
1056 # but only sets the charset parameter in the content-type header when
1057 # a content-type is NOT provided.
1058 return HttpResponse(
1059- get_enlist_userdata(rack_controller=rack_controller),
1060+ get_enlist_userdata(
1061+ rack_controller=rack_controller,
1062+ default_region_ip=default_region_ip),
1063 content_type="text/plain")
1064
1065
1066@@ -1060,9 +1077,10 @@ class AnonMetaDataHandler(VersionIndexHandler):
1067 # non-binary content using DEFAULT_CHARSET (which is UTF-8 by default)
1068 # but only sets the charset parameter in the content-type header when
1069 # a content-type is NOT provided.
1070- return HttpResponse(
1071- get_enlist_preseed(rack_controller=rack_controller),
1072- content_type="text/plain")
1073+ region_ip = get_default_region_ip(request)
1074+ preseed = get_enlist_preseed(
1075+ rack_controller=rack_controller, default_region_ip=region_ip)
1076+ return HttpResponse(preseed, content_type="text/plain")
1077
1078 @operation(idempotent=True)
1079 def get_preseed(self, request, version=None, system_id=None):
1080@@ -1072,7 +1090,9 @@ class AnonMetaDataHandler(VersionIndexHandler):
1081 # non-binary content using DEFAULT_CHARSET (which is UTF-8 by default)
1082 # but only sets the charset parameter in the content-type header when
1083 # a content-type is NOT provided.
1084- return HttpResponse(get_preseed(node), content_type="text/plain")
1085+ region_ip = get_default_region_ip(request)
1086+ preseed = get_preseed(node, region_ip)
1087+ return HttpResponse(preseed, content_type="text/plain")
1088
1089 @operation(idempotent=False)
1090 def netboot_off(self, request, version=None, system_id=None):
1091diff --git a/src/metadataserver/tests/test_api.py b/src/metadataserver/tests/test_api.py
1092index bc6c8c7..140e99b 100644
1093--- a/src/metadataserver/tests/test_api.py
1094+++ b/src/metadataserver/tests/test_api.py
1095@@ -27,6 +27,7 @@ from unittest.mock import (
1096
1097 from django.conf import settings
1098 from django.core.exceptions import PermissionDenied
1099+from provisioningserver.utils.network import get_source_address
1100
1101
1102 try:
1103@@ -2482,6 +2483,22 @@ class TestAnonymousAPI(MAASServerTestCase):
1104 response.content.decode(settings.DEFAULT_CHARSET),
1105 Contains(url))
1106
1107+ def test_anonymous_get_enlist_preseed_uses_detected_region_ip(self):
1108+ request_ip = get_source_address('8.8.8.8')
1109+ expected_source_ip = get_source_address(request_ip)
1110+ rack = factory.make_RackController(url='')
1111+ find_rack_controller_mock = self.patch(api, "find_rack_controller")
1112+ find_rack_controller_mock.return_value = rack
1113+ get_default_region_ip_mock = self.patch(api, "get_default_region_ip")
1114+ get_default_region_ip_mock.return_value = expected_source_ip
1115+ anon_enlist_preseed_url = reverse(
1116+ 'metadata-enlist-preseed', args=['latest'])
1117+ response = self.client.get(
1118+ anon_enlist_preseed_url, {'op': 'get_enlist_preseed'},
1119+ REMOTE_ADDR=request_ip)
1120+ self.assertThat(response.content.decode(
1121+ settings.DEFAULT_CHARSET), Contains(expected_source_ip))
1122+
1123 def test_anonymous_get_preseed(self):
1124 # The preseed for a node can be obtained anonymously.
1125 node = factory.make_Node()
1126@@ -2490,9 +2507,8 @@ class TestAnonymousAPI(MAASServerTestCase):
1127 args=['latest', node.system_id])
1128 # Fake the preseed so we're just exercising the view.
1129 fake_preseed = factory.make_string()
1130- self.patch(api, "get_preseed", lambda node: fake_preseed)
1131- response = self.client.get(
1132- anon_node_url, {'op': 'get_preseed'})
1133+ self.patch(api, "get_preseed", lambda node, ip: fake_preseed)
1134+ response = self.client.get(anon_node_url, {'op': 'get_preseed'})
1135 self.assertEqual(
1136 (http.client.OK.value,
1137 "text/plain",
1138diff --git a/src/provisioningserver/utils/url.py b/src/provisioningserver/utils/url.py
1139index df83ffd..b841c80 100644
1140--- a/src/provisioningserver/utils/url.py
1141+++ b/src/provisioningserver/utils/url.py
1142@@ -18,13 +18,14 @@ import urllib.request
1143
1144
1145 def compose_URL(base_url, host):
1146- """Produce a URL on a given hostname or IP address.
1147+ """Compose (or recompose) a URL, based on an existing URL and given host.
1148
1149 This is straightforward if the IP address is a hostname or an IPv4
1150 address; but if it's an IPv6 address, the URL must contain the IP address
1151 in square brackets as per RFC 3986.
1152
1153- :param base_url: URL without the host part, e.g. `http:///path'.
1154+ :param base_url: URL with or without the host part; for example:
1155+ `http:///path`, `http://foo:5240/path`, or `http://:5240/path`.
1156 :param host: Host name or IP address to insert in the host part of the URL.
1157 :return: A URL string with the host part taken from `host`, and all others
1158 from `base_url`.

Subscribers

People subscribed via source and target branches