Merge lp:~jtv/maas/import-from-configured-mirror into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 1339
Proposed branch: lp:~jtv/maas/import-from-configured-mirror
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 231 lines (+90/-11)
8 files modified
etc/maas/import_ephemerals (+8/-1)
scripts/maas-import-ephemerals (+8/-5)
scripts/maas-import-pxe-files (+2/-1)
src/maasserver/models/nodegroup.py (+10/-1)
src/maasserver/tests/test_nodegroup.py (+26/-0)
src/maasserver/tests/test_views_settings.py (+5/-2)
src/provisioningserver/tasks.py (+8/-1)
src/provisioningserver/tests/test_tasks.py (+23/-0)
To merge this branch: bzr merge lp:~jtv/maas/import-from-configured-mirror
Reviewer Review Type Date Requested Status
Julian Edwards (community) Needs Fixing
Raphaël Badin (community) Approve
Review via email: mp+133505@code.launchpad.net

Commit message

Tell the import scripts, when run as a celery task, what archives to use.

Description of the change

This ties in with Raphaël's ongoing work: he's changing the UI to manage these same settings. I cribbed the settings' names from lp:~rvb/maas/mirror-settings-re-2

Archive locations are passed to the script through environment variables. The scripts already obeys these variables. The impact on the scripts themselves is altogether quite minimal, but one name in maas-import-ephemerals needed changing for uniformity. I added and documented some compatibility code.

URLs that have not been configured yet fall back on the defaults that are encoded in the import scripts. Missing archive locations are not passed through.

Tests pass for this branch even though it does not incorporate Raphaël's branches, which haven't landed yet. That's because my tests configure the archive settings themselves. In a separate branch, which relies on the synthesis of this branch and Raphaël's, I will add an integration test to establish that the two sets of changes operate on the same configuration items.

My test code repeats make_archive_url. It might be worth abstracting a single make_url somewhere that's available to all apps. But then the sensible step would be to sanitize all code that constructs URLs. A job best left to a separate branch!

Jeroen

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Looks good!

[0]

96 + config = {
97 + name: Config.objects.get_config(name)
98 + for name in config_parameters}
99 + task_kwargs = {
100 + name: value
101 + for name, value in config.items()
102 + if value is not None}

I don't see why we need to filter out None values…? Also, there is probably no need for a second dict comprehension.

[1]

233 + archive_settings = {
234 + variable: value
235 + for variable, value in env.iteritems()
236 + if variable.endswith('_ARCHIVE')}

and

99 + task_kwargs = {
100 + name: value
101 + for name, value in config.items()
102 + if value is not None}

A word about style here. You may disagree but I'd be in favor of formatting that kind of statement this way, mainly because I think it's more readable

    task_kwargs = {
        name: value
        for name, value in config.items()
        if value is not None
    }

With your version I get the feeling that the "if value is not None" statement is inside the loop, which isn't true.

[2]

144 + archive_options = {
145 + arg: value
146 + for arg, value in kwargs.items()
147 + if arg.endswith('_archive')}

How about (and this is not only about style ;))

    archive_options = {
        arg: value
        for arg, value in kwargs.items()
        if arg in archives
    }

[3]

> My test code repeats make_archive_url. It might be worth abstracting a single make_url somewhere that's available
> to all apps. But then the sensible step would be to sanitize all code that constructs URLs. A job best left to a
> separate branch!

My code in lp:~rvb/maas/mirror-settings-re-2 could benefit from that too!

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> [0]
>
> 96 + config = {
> 97 + name: Config.objects.get_config(name)
> 98 + for name in config_parameters}
> 99 + task_kwargs = {
> 100 + name: value
> 101 + for name, value in config.items()
> 102 + if value is not None}
>
> I don't see why we need to filter out None values…? Also, there is probably
> no need for a second dict comprehension.

True, it can be done with just one. I thought it'd be hard to read, but it's not so bad so I changed it to just one.

I filtered out the None values because I didn't feel like making the “pass None for default” part of the task's signature. Normally it'd be fine but since a Celery task is a fairly special function-call boundary, I wanted to leave it some room for change. I'm not sure it really helps, but it lets the task specify usefully specify a default other than None, and it helped decouple introduction of the new parameters from introduction of the new config items.

> [1]
>
> 233 + archive_settings = {
> 234 + variable: value
> 235 + for variable, value in env.iteritems()
> 236 + if variable.endswith('_ARCHIVE')}
>
> and
>
> 99 + task_kwargs = {
> 100 + name: value
> 101 + for name, value in config.items()
> 102 + if value is not None}
>
> A word about style here. You may disagree but I'd be in favor of formatting
> that kind of statement this way, mainly because I think it's more readable
>
> task_kwargs = {
> name: value
> for name, value in config.items()
> if value is not None
> }
>
> With your version I get the feeling that the "if value is not None" statement
> is inside the loop, which isn't true.

My editor insists the indentation is the only right way, and I'm reluctant to second-guess it...

The practice of closing a comprehension on the same line goes way back in Launchpad history. I used to do it on a separate line but I think we decided against it eventually. May have been because of controversy over how far a closing bracket (or by extension, brace) should be indented.

> [2]
>
> 144 + archive_options = {
> 145 + arg: value
> 146 + for arg, value in kwargs.items()
> 147 + if arg.endswith('_archive')}
>
> How about (and this is not only about style ;))
>
> archive_options = {
> arg: value
> for arg, value in kwargs.items()
> if arg in archives
> }

I'll do you one better!

    archive_options = {arg: kwargs.get('arg') for arg in archives}

> [3]
>
> > My test code repeats make_archive_url. It might be worth abstracting a
> single make_url somewhere that's available
> > to all apps. But then the sensible step would be to sanitize all code that
> constructs URLs. A job best left to a
> > separate branch!
>
> My code in lp:~rvb/maas/mirror-settings-re-2 could benefit from that too!

A nice little cleanup for when we have time. *Cough* *cough*. :)

Jeroen

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Thursday 08 November 2012 16:07:29 you wrote:
> + config_parameters = {
> + 'http_proxy',
> + 'main_archive',
> + 'ports_archive',
> + 'cloud_images_archive',
> + }

How many places do we refer to these strings now? About time for some
encapsulation or at least a constant, as this is a recipe for disaster.

Especially when you start doing this in tests:

+ archives = {
+ 'main_archive': make_archive_url('main'),
+ 'ports_archive': make_archive_url('ports'),
+ 'cloud_images_archive': make_archive_url('cloud_images'),
+ }
+ for key, value in archives.items():
+ Config.objects.set_config(key, value)

review: Needs Fixing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'etc/maas/import_ephemerals'
--- etc/maas/import_ephemerals 2012-08-15 12:54:49 +0000
+++ etc/maas/import_ephemerals 2012-11-08 17:32:21 +0000
@@ -1,7 +1,14 @@
1## get default settings from import_pxe_files1## get default settings from import_pxe_files
2[ ! -f /etc/maas/import_pxe_files ] || . /etc/maas/import_pxe_files2[ ! -f /etc/maas/import_pxe_files ] || . /etc/maas/import_pxe_files
33
4#REMOTE_IMAGES_MIRROR="https://cloud-images.ubuntu.com"4# CLOUD_IMAGES_ARCHIVE sets the URL where the script can download Ubuntu
5# cloud images. This used to be called REMOTE_IMAGES_MIRROR, and the old
6# name is still supported for compatibility with older installations.
7if [ -n "$REMOTE_IMAGES_MIRROR" ]; then
8 CLOUD_IMAGES_ARCHIVE="$REMOTE_IMAGES_MIRROR"
9fi
10#CLOUD_IMAGES_ARCHIVE="https://cloud-images.ubuntu.com"
11
5#TARGET_NAME_PREFIX="iqn.2004-05.com.ubuntu:maas:"12#TARGET_NAME_PREFIX="iqn.2004-05.com.ubuntu:maas:"
6#DATA_DIR="/var/lib/maas/ephemeral"13#DATA_DIR="/var/lib/maas/ephemeral"
7#RELEASES="precise"14#RELEASES="precise"
815
=== modified file 'scripts/maas-import-ephemerals'
--- scripts/maas-import-ephemerals 2012-11-08 06:34:48 +0000
+++ scripts/maas-import-ephemerals 2012-11-08 17:32:21 +0000
@@ -20,7 +20,10 @@
20# along with this program. If not, see <http://www.gnu.org/licenses/>.20# along with this program. If not, see <http://www.gnu.org/licenses/>.
2121
22VERBOSITY=022VERBOSITY=0
23REMOTE_IMAGES_MIRROR=${REMOTE_IMAGES_MIRROR:-https://maas.ubuntu.com/images}23
24# Mirror to load cloud images from. When the cluster controller runs the
25# import scripts, it provides a setting from the server side.
26CLOUD_IMAGES_ARCHIVE=${CLOUD_IMAGES_ARCHIVE:-https://maas.ubuntu.com/images}
2427
25# iSCSI targets configuration file.28# iSCSI targets configuration file.
26SYS_TGT_CONF="/etc/tgt/targets.conf"29SYS_TGT_CONF="/etc/tgt/targets.conf"
@@ -89,10 +92,10 @@
8992
9093
91query_remote() {94query_remote() {
92 # query /query data at REMOTE_IMAGES_MIRROR95 # query /query data at CLOUD_IMAGES_ARCHIVE
93 # returns 7 values prefixed with 'r_'96 # returns 7 values prefixed with 'r_'
94 local iarch=$1 irelease=$2 istream=$3 out=""97 local iarch=$1 irelease=$2 istream=$3 out=""
95 local burl="${REMOTE_IMAGES_MIRROR}/query"98 local burl="${CLOUD_IMAGES_ARCHIVE}/query"
96 local url="$burl/$irelease/$istream/${STREAM}-dl.current.txt"99 local url="$burl/$irelease/$istream/${STREAM}-dl.current.txt"
97 local target="$TEMP_D/query/$release.$stream"100 local target="$TEMP_D/query/$release.$stream"
98 mkdir -p -- "$TEMP_D/query"101 mkdir -p -- "$TEMP_D/query"
@@ -168,7 +171,7 @@
168 local wd="$1" exdir="" tarball=""171 local wd="$1" exdir="" tarball=""
169 shift172 shift
170 local release=$1 stream=$2 label=$3 serial=$4 arch=$5 url=$6 name=$7173 local release=$1 stream=$2 label=$3 serial=$4 arch=$5 url=$6 name=$7
171 local furl="$REMOTE_IMAGES_MIRROR/$url"174 local furl="$CLOUD_IMAGES_ARCHIVE/$url"
172175
173 mkdir -p "$wd"176 mkdir -p "$wd"
174 cat > "$wd/info" <<EOF177 cat > "$wd/info" <<EOF
@@ -365,7 +368,7 @@
365 query_local "$arch" "$release" "$BUILD_NAME" ||368 query_local "$arch" "$release" "$BUILD_NAME" ||
366 fail "failed to query local for $release/$arch"369 fail "failed to query local for $release/$arch"
367 query_remote "$arch" "$release" "$BUILD_NAME" ||370 query_remote "$arch" "$release" "$BUILD_NAME" ||
368 fail "remote query of $REMOTE_IMAGES_MIRROR failed"371 fail "remote query of $CLOUD_IMAGES_ARCHIVE failed"
369372
370 info="rel: $r_release, arch: $arch: name: $r_name"373 info="rel: $r_release, arch: $arch: name: $r_name"
371 debug 2 "$info"374 debug 2 "$info"
372375
=== modified file 'scripts/maas-import-pxe-files'
--- scripts/maas-import-pxe-files 2012-11-08 06:34:48 +0000
+++ scripts/maas-import-pxe-files 2012-11-08 17:32:21 +0000
@@ -20,7 +20,8 @@
20local_settings="$(pwd)/$settings"20local_settings="$(pwd)/$settings"
21[ -r $local_settings ] && . $local_settings21[ -r $local_settings ] && . $local_settings
2222
23# Download locations for Ubuntu releases.23# Download locations for Ubuntu releases. When the cluster controller runs
24# the import scripts, it provides settings from the server side.
24MAIN_ARCHIVE=${MAIN_ARCHIVE:-http://archive.ubuntu.com/ubuntu/}25MAIN_ARCHIVE=${MAIN_ARCHIVE:-http://archive.ubuntu.com/ubuntu/}
25PORTS_ARCHIVE=${PORTS_ARCHIVE:-http://ports.ubuntu.com/ubuntu-ports/}26PORTS_ARCHIVE=${PORTS_ARCHIVE:-http://ports.ubuntu.com/ubuntu-ports/}
2627
2728
=== modified file 'src/maasserver/models/nodegroup.py'
--- src/maasserver/models/nodegroup.py 2012-11-08 06:34:48 +0000
+++ src/maasserver/models/nodegroup.py 2012-11-08 17:32:21 +0000
@@ -239,7 +239,16 @@
239 """239 """
240 # Avoid circular imports.240 # Avoid circular imports.
241 from maasserver.models import Config241 from maasserver.models import Config
242 task_kwargs = dict(http_proxy=Config.objects.get_config('http_proxy'))242 config_parameters = {
243 'http_proxy',
244 'main_archive',
245 'ports_archive',
246 'cloud_images_archive',
247 }
248 task_kwargs = {
249 name: Config.objects.get_config(name)
250 for name in config_parameters
251 if Config.objects.get_config(name) is not None}
243 import_boot_images.apply_async(queue=self.uuid, kwargs=task_kwargs)252 import_boot_images.apply_async(queue=self.uuid, kwargs=task_kwargs)
244253
245 def add_dhcp_host_maps(self, new_leases):254 def add_dhcp_host_maps(self, new_leases):
246255
=== modified file 'src/maasserver/tests/test_nodegroup.py'
--- src/maasserver/tests/test_nodegroup.py 2012-11-07 10:39:19 +0000
+++ src/maasserver/tests/test_nodegroup.py 2012-11-08 17:32:21 +0000
@@ -265,6 +265,14 @@
265 self.assertItemsEqual(calls, recorder.apply_async.call_args_list)265 self.assertItemsEqual(calls, recorder.apply_async.call_args_list)
266266
267267
268def make_archive_url(name):
269 """Create a fake archive URL."""
270 return "http://%s.example.com/%s/" % (
271 factory.make_name(name),
272 factory.make_name('path'),
273 )
274
275
268class TestNodeGroup(TestCase):276class TestNodeGroup(TestCase):
269277
270 resources = (278 resources = (
@@ -373,6 +381,24 @@
373 recorder.assert_called_once_with(381 recorder.assert_called_once_with(
374 ['sudo', '-n', 'maas-import-pxe-files'], env=expected_env)382 ['sudo', '-n', 'maas-import-pxe-files'], env=expected_env)
375383
384 def test_import_boot_images_selects_archive_locations_from_config(self):
385 recorder = self.patch(nodegroup_module, 'import_boot_images')
386 nodegroup = factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED)
387
388 archives = {
389 'main_archive': make_archive_url('main'),
390 'ports_archive': make_archive_url('ports'),
391 'cloud_images_archive': make_archive_url('cloud_images'),
392 }
393 for key, value in archives.items():
394 Config.objects.set_config(key, value)
395
396 nodegroup.import_boot_images()
397
398 kwargs = recorder.apply_async.call_args[1]['kwargs']
399 archive_options = {arg: kwargs.get(arg) for arg in archives}
400 self.assertEqual(archives, archive_options)
401
376 def test_import_boot_images_sent_to_nodegroup_queue(self):402 def test_import_boot_images_sent_to_nodegroup_queue(self):
377 recorder = self.patch(nodegroup_module, 'import_boot_images', Mock())403 recorder = self.patch(nodegroup_module, 'import_boot_images', Mock())
378 nodegroup = factory.make_node_group()404 nodegroup = factory.make_node_group()
379405
=== modified file 'src/maasserver/tests/test_views_settings.py'
--- src/maasserver/tests/test_views_settings.py 2012-11-08 10:40:48 +0000
+++ src/maasserver/tests/test_views_settings.py 2012-11-08 17:32:21 +0000
@@ -38,7 +38,10 @@
38 AdminLoggedInTestCase,38 AdminLoggedInTestCase,
39 LoggedInTestCase,39 LoggedInTestCase,
40 )40 )
41from mock import call41from mock import (
42 ANY,
43 call,
44 )
4245
4346
44class SettingsTest(AdminLoggedInTestCase):47class SettingsTest(AdminLoggedInTestCase):
@@ -248,7 +251,7 @@
248 reverse('settings'), {'import_all_boot_images': 1})251 reverse('settings'), {'import_all_boot_images': 1})
249 self.assertEqual(httplib.FOUND, response.status_code)252 self.assertEqual(httplib.FOUND, response.status_code)
250 calls = [253 calls = [
251 call(queue=nodegroup.work_queue, kwargs={'http_proxy': None})254 call(queue=nodegroup.work_queue, kwargs=ANY)
252 for nodegroup in accepted_nodegroups255 for nodegroup in accepted_nodegroups
253 ]256 ]
254 self.assertItemsEqual(calls, recorder.apply_async.call_args_list)257 self.assertItemsEqual(calls, recorder.apply_async.call_args_list)
255258
=== modified file 'src/provisioningserver/tasks.py'
--- src/provisioningserver/tasks.py 2012-11-08 06:34:48 +0000
+++ src/provisioningserver/tasks.py 2012-11-08 17:32:21 +0000
@@ -376,9 +376,16 @@
376# =====================================================================376# =====================================================================
377377
378@task378@task
379def import_boot_images(http_proxy=None):379def import_boot_images(http_proxy=None, main_archive=None, ports_archive=None,
380 cloud_images_archive=None):
380 env = dict(os.environ)381 env = dict(os.environ)
381 if http_proxy is not None:382 if http_proxy is not None:
382 env['http_proxy'] = http_proxy383 env['http_proxy'] = http_proxy
383 env['https_proxy'] = http_proxy384 env['https_proxy'] = http_proxy
385 if main_archive is not None:
386 env['MAIN_ARCHIVE'] = main_archive
387 if ports_archive is not None:
388 env['PORTS_ARCHIVE'] = ports_archive
389 if cloud_images_archive is not None:
390 env['CLOUD_IMAGES_ARCHIVE'] = cloud_images_archive
384 check_call(['sudo', '-n', 'maas-import-pxe-files'], env=env)391 check_call(['sudo', '-n', 'maas-import-pxe-files'], env=env)
385392
=== modified file 'src/provisioningserver/tests/test_tasks.py'
--- src/provisioningserver/tests/test_tasks.py 2012-11-08 06:34:48 +0000
+++ src/provisioningserver/tests/test_tasks.py 2012-11-08 17:32:21 +0000
@@ -540,6 +540,11 @@
540540
541class TestImportPxeFiles(PservTestCase):541class TestImportPxeFiles(PservTestCase):
542542
543 def make_archive_url(self, name=None):
544 if name is None:
545 name = factory.make_name('archive')
546 return 'http://%s.example.com/%s' % (name, factory.make_name('path'))
547
543 def test_import_boot_images(self):548 def test_import_boot_images(self):
544 recorder = self.patch(tasks, 'check_call', Mock())549 recorder = self.patch(tasks, 'check_call', Mock())
545 import_boot_images()550 import_boot_images()
@@ -560,3 +565,21 @@
560 expected_env = dict(os.environ, http_proxy=proxy, https_proxy=proxy)565 expected_env = dict(os.environ, http_proxy=proxy, https_proxy=proxy)
561 recorder.assert_called_once_with(566 recorder.assert_called_once_with(
562 ['sudo', '-n', 'maas-import-pxe-files'], env=expected_env)567 ['sudo', '-n', 'maas-import-pxe-files'], env=expected_env)
568
569 def test_import_boot_images_sets_archive_locations(self):
570 self.patch(tasks, 'check_call')
571 archives = {
572 'main_archive': self.make_archive_url('main'),
573 'ports_archive': self.make_archive_url('ports'),
574 'cloud_images_archive': self.make_archive_url('cloud-images'),
575 }
576 expected_settings = {
577 parameter.upper(): value
578 for parameter, value in archives.items()}
579 import_boot_images(**archives)
580 env = tasks.check_call.call_args[1]['env']
581 archive_settings = {
582 variable: value
583 for variable, value in env.iteritems()
584 if variable.endswith('_ARCHIVE')}
585 self.assertEqual(expected_settings, archive_settings)