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
1=== modified file 'etc/maas/import_ephemerals'
2--- etc/maas/import_ephemerals 2012-08-15 12:54:49 +0000
3+++ etc/maas/import_ephemerals 2012-11-08 17:32:21 +0000
4@@ -1,7 +1,14 @@
5 ## get default settings from import_pxe_files
6 [ ! -f /etc/maas/import_pxe_files ] || . /etc/maas/import_pxe_files
7
8-#REMOTE_IMAGES_MIRROR="https://cloud-images.ubuntu.com"
9+# CLOUD_IMAGES_ARCHIVE sets the URL where the script can download Ubuntu
10+# cloud images. This used to be called REMOTE_IMAGES_MIRROR, and the old
11+# name is still supported for compatibility with older installations.
12+if [ -n "$REMOTE_IMAGES_MIRROR" ]; then
13+ CLOUD_IMAGES_ARCHIVE="$REMOTE_IMAGES_MIRROR"
14+fi
15+#CLOUD_IMAGES_ARCHIVE="https://cloud-images.ubuntu.com"
16+
17 #TARGET_NAME_PREFIX="iqn.2004-05.com.ubuntu:maas:"
18 #DATA_DIR="/var/lib/maas/ephemeral"
19 #RELEASES="precise"
20
21=== modified file 'scripts/maas-import-ephemerals'
22--- scripts/maas-import-ephemerals 2012-11-08 06:34:48 +0000
23+++ scripts/maas-import-ephemerals 2012-11-08 17:32:21 +0000
24@@ -20,7 +20,10 @@
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
27 VERBOSITY=0
28-REMOTE_IMAGES_MIRROR=${REMOTE_IMAGES_MIRROR:-https://maas.ubuntu.com/images}
29+
30+# Mirror to load cloud images from. When the cluster controller runs the
31+# import scripts, it provides a setting from the server side.
32+CLOUD_IMAGES_ARCHIVE=${CLOUD_IMAGES_ARCHIVE:-https://maas.ubuntu.com/images}
33
34 # iSCSI targets configuration file.
35 SYS_TGT_CONF="/etc/tgt/targets.conf"
36@@ -89,10 +92,10 @@
37
38
39 query_remote() {
40- # query /query data at REMOTE_IMAGES_MIRROR
41+ # query /query data at CLOUD_IMAGES_ARCHIVE
42 # returns 7 values prefixed with 'r_'
43 local iarch=$1 irelease=$2 istream=$3 out=""
44- local burl="${REMOTE_IMAGES_MIRROR}/query"
45+ local burl="${CLOUD_IMAGES_ARCHIVE}/query"
46 local url="$burl/$irelease/$istream/${STREAM}-dl.current.txt"
47 local target="$TEMP_D/query/$release.$stream"
48 mkdir -p -- "$TEMP_D/query"
49@@ -168,7 +171,7 @@
50 local wd="$1" exdir="" tarball=""
51 shift
52 local release=$1 stream=$2 label=$3 serial=$4 arch=$5 url=$6 name=$7
53- local furl="$REMOTE_IMAGES_MIRROR/$url"
54+ local furl="$CLOUD_IMAGES_ARCHIVE/$url"
55
56 mkdir -p "$wd"
57 cat > "$wd/info" <<EOF
58@@ -365,7 +368,7 @@
59 query_local "$arch" "$release" "$BUILD_NAME" ||
60 fail "failed to query local for $release/$arch"
61 query_remote "$arch" "$release" "$BUILD_NAME" ||
62- fail "remote query of $REMOTE_IMAGES_MIRROR failed"
63+ fail "remote query of $CLOUD_IMAGES_ARCHIVE failed"
64
65 info="rel: $r_release, arch: $arch: name: $r_name"
66 debug 2 "$info"
67
68=== modified file 'scripts/maas-import-pxe-files'
69--- scripts/maas-import-pxe-files 2012-11-08 06:34:48 +0000
70+++ scripts/maas-import-pxe-files 2012-11-08 17:32:21 +0000
71@@ -20,7 +20,8 @@
72 local_settings="$(pwd)/$settings"
73 [ -r $local_settings ] && . $local_settings
74
75-# Download locations for Ubuntu releases.
76+# Download locations for Ubuntu releases. When the cluster controller runs
77+# the import scripts, it provides settings from the server side.
78 MAIN_ARCHIVE=${MAIN_ARCHIVE:-http://archive.ubuntu.com/ubuntu/}
79 PORTS_ARCHIVE=${PORTS_ARCHIVE:-http://ports.ubuntu.com/ubuntu-ports/}
80
81
82=== modified file 'src/maasserver/models/nodegroup.py'
83--- src/maasserver/models/nodegroup.py 2012-11-08 06:34:48 +0000
84+++ src/maasserver/models/nodegroup.py 2012-11-08 17:32:21 +0000
85@@ -239,7 +239,16 @@
86 """
87 # Avoid circular imports.
88 from maasserver.models import Config
89- task_kwargs = dict(http_proxy=Config.objects.get_config('http_proxy'))
90+ config_parameters = {
91+ 'http_proxy',
92+ 'main_archive',
93+ 'ports_archive',
94+ 'cloud_images_archive',
95+ }
96+ task_kwargs = {
97+ name: Config.objects.get_config(name)
98+ for name in config_parameters
99+ if Config.objects.get_config(name) is not None}
100 import_boot_images.apply_async(queue=self.uuid, kwargs=task_kwargs)
101
102 def add_dhcp_host_maps(self, new_leases):
103
104=== modified file 'src/maasserver/tests/test_nodegroup.py'
105--- src/maasserver/tests/test_nodegroup.py 2012-11-07 10:39:19 +0000
106+++ src/maasserver/tests/test_nodegroup.py 2012-11-08 17:32:21 +0000
107@@ -265,6 +265,14 @@
108 self.assertItemsEqual(calls, recorder.apply_async.call_args_list)
109
110
111+def make_archive_url(name):
112+ """Create a fake archive URL."""
113+ return "http://%s.example.com/%s/" % (
114+ factory.make_name(name),
115+ factory.make_name('path'),
116+ )
117+
118+
119 class TestNodeGroup(TestCase):
120
121 resources = (
122@@ -373,6 +381,24 @@
123 recorder.assert_called_once_with(
124 ['sudo', '-n', 'maas-import-pxe-files'], env=expected_env)
125
126+ def test_import_boot_images_selects_archive_locations_from_config(self):
127+ recorder = self.patch(nodegroup_module, 'import_boot_images')
128+ nodegroup = factory.make_node_group(status=NODEGROUP_STATUS.ACCEPTED)
129+
130+ archives = {
131+ 'main_archive': make_archive_url('main'),
132+ 'ports_archive': make_archive_url('ports'),
133+ 'cloud_images_archive': make_archive_url('cloud_images'),
134+ }
135+ for key, value in archives.items():
136+ Config.objects.set_config(key, value)
137+
138+ nodegroup.import_boot_images()
139+
140+ kwargs = recorder.apply_async.call_args[1]['kwargs']
141+ archive_options = {arg: kwargs.get(arg) for arg in archives}
142+ self.assertEqual(archives, archive_options)
143+
144 def test_import_boot_images_sent_to_nodegroup_queue(self):
145 recorder = self.patch(nodegroup_module, 'import_boot_images', Mock())
146 nodegroup = factory.make_node_group()
147
148=== modified file 'src/maasserver/tests/test_views_settings.py'
149--- src/maasserver/tests/test_views_settings.py 2012-11-08 10:40:48 +0000
150+++ src/maasserver/tests/test_views_settings.py 2012-11-08 17:32:21 +0000
151@@ -38,7 +38,10 @@
152 AdminLoggedInTestCase,
153 LoggedInTestCase,
154 )
155-from mock import call
156+from mock import (
157+ ANY,
158+ call,
159+ )
160
161
162 class SettingsTest(AdminLoggedInTestCase):
163@@ -248,7 +251,7 @@
164 reverse('settings'), {'import_all_boot_images': 1})
165 self.assertEqual(httplib.FOUND, response.status_code)
166 calls = [
167- call(queue=nodegroup.work_queue, kwargs={'http_proxy': None})
168+ call(queue=nodegroup.work_queue, kwargs=ANY)
169 for nodegroup in accepted_nodegroups
170 ]
171 self.assertItemsEqual(calls, recorder.apply_async.call_args_list)
172
173=== modified file 'src/provisioningserver/tasks.py'
174--- src/provisioningserver/tasks.py 2012-11-08 06:34:48 +0000
175+++ src/provisioningserver/tasks.py 2012-11-08 17:32:21 +0000
176@@ -376,9 +376,16 @@
177 # =====================================================================
178
179 @task
180-def import_boot_images(http_proxy=None):
181+def import_boot_images(http_proxy=None, main_archive=None, ports_archive=None,
182+ cloud_images_archive=None):
183 env = dict(os.environ)
184 if http_proxy is not None:
185 env['http_proxy'] = http_proxy
186 env['https_proxy'] = http_proxy
187+ if main_archive is not None:
188+ env['MAIN_ARCHIVE'] = main_archive
189+ if ports_archive is not None:
190+ env['PORTS_ARCHIVE'] = ports_archive
191+ if cloud_images_archive is not None:
192+ env['CLOUD_IMAGES_ARCHIVE'] = cloud_images_archive
193 check_call(['sudo', '-n', 'maas-import-pxe-files'], env=env)
194
195=== modified file 'src/provisioningserver/tests/test_tasks.py'
196--- src/provisioningserver/tests/test_tasks.py 2012-11-08 06:34:48 +0000
197+++ src/provisioningserver/tests/test_tasks.py 2012-11-08 17:32:21 +0000
198@@ -540,6 +540,11 @@
199
200 class TestImportPxeFiles(PservTestCase):
201
202+ def make_archive_url(self, name=None):
203+ if name is None:
204+ name = factory.make_name('archive')
205+ return 'http://%s.example.com/%s' % (name, factory.make_name('path'))
206+
207 def test_import_boot_images(self):
208 recorder = self.patch(tasks, 'check_call', Mock())
209 import_boot_images()
210@@ -560,3 +565,21 @@
211 expected_env = dict(os.environ, http_proxy=proxy, https_proxy=proxy)
212 recorder.assert_called_once_with(
213 ['sudo', '-n', 'maas-import-pxe-files'], env=expected_env)
214+
215+ def test_import_boot_images_sets_archive_locations(self):
216+ self.patch(tasks, 'check_call')
217+ archives = {
218+ 'main_archive': self.make_archive_url('main'),
219+ 'ports_archive': self.make_archive_url('ports'),
220+ 'cloud_images_archive': self.make_archive_url('cloud-images'),
221+ }
222+ expected_settings = {
223+ parameter.upper(): value
224+ for parameter, value in archives.items()}
225+ import_boot_images(**archives)
226+ env = tasks.check_call.call_args[1]['env']
227+ archive_settings = {
228+ variable: value
229+ for variable, value in env.iteritems()
230+ if variable.endswith('_ARCHIVE')}
231+ self.assertEqual(expected_settings, archive_settings)