Merge ~mpontillo/maas:better-default-maas-url--bug-1418044 into maas:master
- Git
- lp:~mpontillo/maas
- better-default-maas-url--bug-1418044
- Merge into 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) |
Related bugs: |
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.
Description of the change
To post a comment you must log in.
- c01a722... by Mike Pontillo
-
Fix tests.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/src/maasserver/compose_preseed.py b/src/maasserver/compose_preseed.py |
2 | index 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) |
249 | diff --git a/src/maasserver/preseed.py b/src/maasserver/preseed.py |
250 | index 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) |
525 | diff --git a/src/maasserver/rpc/boot.py b/src/maasserver/rpc/boot.py |
526 | index 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) |
576 | diff --git a/src/maasserver/rpc/tests/test_boot.py b/src/maasserver/rpc/tests/test_boot.py |
577 | index 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): |
618 | diff --git a/src/maasserver/server_address.py b/src/maasserver/server_address.py |
619 | index 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: |
643 | diff --git a/src/maasserver/tests/test_compose_preseed.py b/src/maasserver/tests/test_compose_preseed.py |
644 | index 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 |
880 | diff --git a/src/maasserver/tests/test_preseed.py b/src/maasserver/tests/test_preseed.py |
881 | index 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): |
920 | diff --git a/src/maasserver/utils/__init__.py b/src/maasserver/utils/__init__.py |
921 | index 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 |
990 | diff --git a/src/metadataserver/api.py b/src/metadataserver/api.py |
991 | index 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): |
1091 | diff --git a/src/metadataserver/tests/test_api.py b/src/metadataserver/tests/test_api.py |
1092 | index 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", |
1138 | diff --git a/src/provisioningserver/utils/url.py b/src/provisioningserver/utils/url.py |
1139 | index 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`. |
UNIT TESTS default- maas-url- -bug-1418044 lp:~mpontillo/maas into -b master lp:~maas-committers/maas
-b better-
STATUS: FAILED maas-ci- jenkins. internal: 8080/job/ maas/job/ branch- tester/ 618/console 29f061ac0994a6f 0f4d78703d
LOG: http://
COMMIT: 25861b7702b4a6a