Merge lp:~harlowja/cloud-init/cloud-init-seedy into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Joshua Harlow
Status: Merged
Merged at revision: 866
Proposed branch: lp:~harlowja/cloud-init/cloud-init-seedy
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 341 lines (+263/-14)
5 files modified
cloudinit/config/cc_seed_random.py (+61/-0)
cloudinit/exceptions.py (+21/-0)
cloudinit/sources/DataSourceConfigDrive.py (+30/-14)
config/cloud.cfg (+1/-0)
tests/unittests/test_handler/test_handler_seed_random.py (+150/-0)
To merge this branch: bzr merge lp:~harlowja/cloud-init/cloud-init-seedy
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+183571@code.launchpad.net

Description of the change

  Add config drive support for random_seed

  A new field in the metadata has emerged, one
  that provides a way to seed the linux random
  generator. Add support for writing the seed
  and rewrite parts of the on_boot code to use
  a little helper class.

To post a comment you must log in.
866. By Joshua Harlow

Raise when no seed filename provided

Revision history for this message
Scott Moser (smoser) wrote :

What about just a cc_dev_random_seed that read from datasource.random_seed() if available ?

The only reason I'd lean that way is that azure also provides random seed data.

We'd just want the cc_dev_random_seed to early in the init_modules.

Revision history for this message
Joshua Harlow (harlowja) wrote :

Sounds good, also do u know if the file paths are right. The ones I found look right for ubunutu/rhel but second verification wouldn't hurt.

Revision history for this message
Scott Moser (smoser) wrote :

Josh,
  I actually think its simpler if you just write to /dev/urandom.
  You should probably make that configurable via cloud-config. but just putting it into /dev/urandom correctly seeds the kernel entropy pool.

867. By Joshua Harlow

Review adjustments.

Revision history for this message
Joshua Harlow (harlowja) wrote :

Ok dokie, adjustments made :) I should probably add a test though.

Revision history for this message
Scott Moser (smoser) wrote :

It looks nice now. Some other thoughts:

 * I don't know if I prefer cloud.distro.set_random_seed() over cfg.get('random_seed_file', '/dev/urandom')
   You have thoughts on that? The latter allows me to change it easily from user-data.
 * I wouldn't prefer the metadata from user-data, but rather just append them. the way /dev/urandom works is that "more is better".
 * I guess we're expecting binary data to be in cfg.get('random_seed'). Thats fine, as yaml actually supports binary data, but might be useful to allow the user to specify that the data is base64 and decode it. I dont know.
 * I *think* I'm moving towards getting stuff out of the top level of cloud_config and into specific dictionaries. Ie:
   random_seed:
    file: /dev/urandom
    encoding: base64
    data: Zm9vYmFyCg==

   As opposed to:
    random_seed_file: /dev/urandom
    random_seed_encoding: /dev/urandom
    random_seed: Zm9vYmFyCg==

 * test would be good. :)

Thanks for doing this.

868. By Joshua Harlow

Add jsonschema for namespaced and verifiable module
configuration checking as well as make most of the
module logic happen in the module itself instead of
interacting with the distro object.

Revision history for this message
Joshua Harlow (harlowja) wrote :

Ok I updated it with some new goodies. No tests yet, but let me know what u think :-)

869. By Joshua Harlow

Ensure validate checks key existence.

Revision history for this message
Scott Moser (smoser) wrote :

Josh,
  I like the direction here. I like the jsonschema, but I'm targetting 0.7.3 for later this month, and i'd rather not pick up the new dependency there. Could we drop the jsonschema stuff for now, and add it post 0.7.3 ?

  I really do like the 'validate' idea though.

  I think for now just take the 'validate' use out, and drop the dependency and we're probably good.
  Of course a test would be nice :)
Thanks.

Revision history for this message
Joshua Harlow (harlowja) wrote :

I, I captain, will do that (later tonight I hope).

870. By Joshua Harlow

Add test + remove jsonschema (for now)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'cloudinit/config/cc_seed_random.py'
2--- cloudinit/config/cc_seed_random.py 1970-01-01 00:00:00 +0000
3+++ cloudinit/config/cc_seed_random.py 2013-09-09 05:48:24 +0000
4@@ -0,0 +1,61 @@
5+# vi: ts=4 expandtab
6+#
7+# Copyright (C) 2013 Yahoo! Inc.
8+#
9+# Author: Joshua Harlow <harlowja@yahoo-inc.com>
10+#
11+# This program is free software: you can redistribute it and/or modify
12+# it under the terms of the GNU General Public License version 3, as
13+# published by the Free Software Foundation.
14+#
15+# This program is distributed in the hope that it will be useful,
16+# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+# GNU General Public License for more details.
19+#
20+# You should have received a copy of the GNU General Public License
21+# along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
23+import base64
24+from StringIO import StringIO
25+
26+from cloudinit.settings import PER_INSTANCE
27+from cloudinit import util
28+
29+frequency = PER_INSTANCE
30+
31+
32+def _decode(data, encoding=None):
33+ if not data:
34+ return ''
35+ if not encoding or encoding.lower() in ['raw']:
36+ return data
37+ elif encoding.lower() in ['base64', 'b64']:
38+ return base64.b64decode(data)
39+ elif encoding.lower() in ['gzip', 'gz']:
40+ return util.decomp_gzip(data, quiet=False)
41+ else:
42+ raise IOError("Unknown random_seed encoding: %s" % (encoding))
43+
44+
45+def handle(name, cfg, cloud, log, _args):
46+ if not cfg or "random_seed" not in cfg:
47+ log.debug(("Skipping module named %s, "
48+ "no 'random_seed' configuration found"), name)
49+ return
50+
51+ my_cfg = cfg['random_seed']
52+ seed_path = my_cfg.get('file', '/dev/urandom')
53+ seed_buf = StringIO()
54+ seed_buf.write(_decode(my_cfg.get('data', ''),
55+ encoding=my_cfg.get('encoding')))
56+
57+ metadata = cloud.datasource.metadata
58+ if metadata and 'random_seed' in metadata:
59+ seed_buf.write(metadata['random_seed'])
60+
61+ seed_data = seed_buf.getvalue()
62+ if len(seed_data):
63+ log.debug("%s: adding %s bytes of random seed entrophy to %s", name,
64+ len(seed_data), seed_path)
65+ util.append_file(seed_path, seed_data)
66
67=== added file 'cloudinit/exceptions.py'
68--- cloudinit/exceptions.py 1970-01-01 00:00:00 +0000
69+++ cloudinit/exceptions.py 2013-09-09 05:48:24 +0000
70@@ -0,0 +1,21 @@
71+# vi: ts=4 expandtab
72+#
73+# Copyright (C) 2013 Yahoo! Inc.
74+#
75+# Author: Joshua Harlow <harlowja@yahoo-inc.com>
76+#
77+# This program is free software: you can redistribute it and/or modify
78+# it under the terms of the GNU General Public License version 3, as
79+# published by the Free Software Foundation.
80+#
81+# This program is distributed in the hope that it will be useful,
82+# but WITHOUT ANY WARRANTY; without even the implied warranty of
83+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
84+# GNU General Public License for more details.
85+#
86+# You should have received a copy of the GNU General Public License
87+# along with this program. If not, see <http://www.gnu.org/licenses/>.
88+
89+
90+class FormatValidationError(Exception):
91+ pass
92
93=== modified file 'cloudinit/sources/DataSourceConfigDrive.py'
94--- cloudinit/sources/DataSourceConfigDrive.py 2013-06-05 00:42:55 +0000
95+++ cloudinit/sources/DataSourceConfigDrive.py 2013-09-09 05:48:24 +0000
96@@ -18,6 +18,7 @@
97 # You should have received a copy of the GNU General Public License
98 # along with this program. If not, see <http://www.gnu.org/licenses/>.
99
100+import base64
101 import json
102 import os
103
104@@ -41,6 +42,25 @@
105 VALID_DSMODES = ("local", "net", "pass", "disabled")
106
107
108+class ConfigDriveHelper(object):
109+ def __init__(self, distro):
110+ self.distro = distro
111+
112+ def on_first_boot(self, data):
113+ if not data:
114+ data = {}
115+ if 'network_config' in data:
116+ LOG.debug("Updating network interfaces from config drive")
117+ self.distro.apply_network(data['network_config'])
118+ files = data.get('files')
119+ if files:
120+ LOG.debug("Writing %s injected files", len(files))
121+ try:
122+ write_files(files)
123+ except IOError:
124+ util.logexc(LOG, "Failed writing files")
125+
126+
127 class DataSourceConfigDrive(sources.DataSource):
128 def __init__(self, sys_cfg, distro, paths):
129 sources.DataSource.__init__(self, sys_cfg, distro, paths)
130@@ -49,6 +69,7 @@
131 self.seed_dir = os.path.join(paths.seed_dir, 'config_drive')
132 self.version = None
133 self.ec2_metadata = None
134+ self.helper = ConfigDriveHelper(distro)
135
136 def __str__(self):
137 root = sources.DataSource.__str__(self)
138@@ -187,20 +208,8 @@
139 # instance-id
140 prev_iid = get_previous_iid(self.paths)
141 cur_iid = md['instance-id']
142-
143- if ('network_config' in results and self.dsmode == "local" and
144- prev_iid != cur_iid):
145- LOG.debug("Updating network interfaces from config drive (%s)",
146- dsmode)
147- self.distro.apply_network(results['network_config'])
148-
149- # file writing occurs in local mode (to be as early as possible)
150- if self.dsmode == "local" and prev_iid != cur_iid and results['files']:
151- LOG.debug("writing injected files")
152- try:
153- write_files(results['files'])
154- except:
155- util.logexc(LOG, "Failed writing files")
156+ if prev_iid != cur_iid and self.dsmode == "local":
157+ self.helper.on_first_boot(results)
158
159 # dsmode != self.dsmode here if:
160 # * dsmode = "pass", pass means it should only copy files and then
161@@ -338,6 +347,13 @@
162 except KeyError:
163 raise BrokenConfigDriveDir("No uuid entry in metadata")
164
165+ if 'random_seed' in results['metadata']:
166+ random_seed = results['metadata']['random_seed']
167+ try:
168+ results['metadata']['random_seed'] = base64.b64decode(random_seed)
169+ except (ValueError, TypeError) as exc:
170+ raise BrokenConfigDriveDir("Badly formatted random_seed: %s" % exc)
171+
172 def read_content_path(item):
173 # do not use os.path.join here, as content_path starts with /
174 cpath = os.path.sep.join((source_dir, "openstack",
175
176=== modified file 'config/cloud.cfg'
177--- config/cloud.cfg 2013-03-01 06:43:06 +0000
178+++ config/cloud.cfg 2013-09-09 05:48:24 +0000
179@@ -24,6 +24,7 @@
180 # The modules that run in the 'init' stage
181 cloud_init_modules:
182 - migrator
183+ - seed_random
184 - bootcmd
185 - write-files
186 - growpart
187
188=== added file 'tests/unittests/test_handler/test_handler_seed_random.py'
189--- tests/unittests/test_handler/test_handler_seed_random.py 1970-01-01 00:00:00 +0000
190+++ tests/unittests/test_handler/test_handler_seed_random.py 2013-09-09 05:48:24 +0000
191@@ -0,0 +1,150 @@
192+ # Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
193+#
194+# Author: Juerg Haefliger <juerg.haefliger@hp.com>
195+#
196+# Based on test_handler_set_hostname.py
197+#
198+# This program is free software: you can redistribute it and/or modify
199+# it under the terms of the GNU General Public License version 3, as
200+# published by the Free Software Foundation.
201+#
202+# This program is distributed in the hope that it will be useful,
203+# but WITHOUT ANY WARRANTY; without even the implied warranty of
204+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
205+# GNU General Public License for more details.
206+#
207+# You should have received a copy of the GNU General Public License
208+# along with this program. If not, see <http://www.gnu.org/licenses/>.
209+
210+from cloudinit.config import cc_seed_random
211+
212+import base64
213+import tempfile
214+import gzip
215+
216+from StringIO import StringIO
217+
218+from cloudinit import cloud
219+from cloudinit import distros
220+from cloudinit import helpers
221+from cloudinit import util
222+
223+from cloudinit.sources import DataSourceNone
224+
225+from tests.unittests import helpers as t_help
226+
227+import logging
228+
229+LOG = logging.getLogger(__name__)
230+
231+
232+class TestRandomSeed(t_help.TestCase):
233+ def setUp(self):
234+ super(TestRandomSeed, self).setUp()
235+ self._seed_file = tempfile.mktemp()
236+
237+ def tearDown(self):
238+ util.del_file(self._seed_file)
239+
240+ def _compress(self, text):
241+ contents = StringIO()
242+ gz_fh = gzip.GzipFile(mode='wb', fileobj=contents)
243+ gz_fh.write(text)
244+ gz_fh.close()
245+ return contents.getvalue()
246+
247+ def _get_cloud(self, distro, metadata=None):
248+ paths = helpers.Paths({})
249+ cls = distros.fetch(distro)
250+ ubuntu_distro = cls(distro, {}, paths)
251+ ds = DataSourceNone.DataSourceNone({}, ubuntu_distro, paths)
252+ if metadata:
253+ ds.metadata = metadata
254+ return cloud.Cloud(ds, paths, {}, ubuntu_distro, None)
255+
256+ def test_append_random(self):
257+ cfg = {
258+ 'random_seed': {
259+ 'file': self._seed_file,
260+ 'data': 'tiny-tim-was-here',
261+ }
262+ }
263+ cc_seed_random.handle('test', cfg, self._get_cloud('ubuntu'), LOG, [])
264+ contents = util.load_file(self._seed_file)
265+ self.assertEquals("tiny-tim-was-here", contents)
266+
267+ def test_append_random_unknown_encoding(self):
268+ data = self._compress("tiny-toe")
269+ cfg = {
270+ 'random_seed': {
271+ 'file': self._seed_file,
272+ 'data': data,
273+ 'encoding': 'special_encoding',
274+ }
275+ }
276+ self.assertRaises(IOError, cc_seed_random.handle, 'test', cfg,
277+ self._get_cloud('ubuntu'), LOG, [])
278+
279+ def test_append_random_gzip(self):
280+ data = self._compress("tiny-toe")
281+ cfg = {
282+ 'random_seed': {
283+ 'file': self._seed_file,
284+ 'data': data,
285+ 'encoding': 'gzip',
286+ }
287+ }
288+ cc_seed_random.handle('test', cfg, self._get_cloud('ubuntu'), LOG, [])
289+ contents = util.load_file(self._seed_file)
290+ self.assertEquals("tiny-toe", contents)
291+
292+ def test_append_random_gz(self):
293+ data = self._compress("big-toe")
294+ cfg = {
295+ 'random_seed': {
296+ 'file': self._seed_file,
297+ 'data': data,
298+ 'encoding': 'gz',
299+ }
300+ }
301+ cc_seed_random.handle('test', cfg, self._get_cloud('ubuntu'), LOG, [])
302+ contents = util.load_file(self._seed_file)
303+ self.assertEquals("big-toe", contents)
304+
305+ def test_append_random_base64(self):
306+ data = base64.b64encode('bubbles')
307+ cfg = {
308+ 'random_seed': {
309+ 'file': self._seed_file,
310+ 'data': data,
311+ 'encoding': 'base64',
312+ }
313+ }
314+ cc_seed_random.handle('test', cfg, self._get_cloud('ubuntu'), LOG, [])
315+ contents = util.load_file(self._seed_file)
316+ self.assertEquals("bubbles", contents)
317+
318+ def test_append_random_b64(self):
319+ data = base64.b64encode('kit-kat')
320+ cfg = {
321+ 'random_seed': {
322+ 'file': self._seed_file,
323+ 'data': data,
324+ 'encoding': 'b64',
325+ }
326+ }
327+ cc_seed_random.handle('test', cfg, self._get_cloud('ubuntu'), LOG, [])
328+ contents = util.load_file(self._seed_file)
329+ self.assertEquals("kit-kat", contents)
330+
331+ def test_append_random_metadata(self):
332+ cfg = {
333+ 'random_seed': {
334+ 'file': self._seed_file,
335+ 'data': 'tiny-tim-was-here',
336+ }
337+ }
338+ c = self._get_cloud('ubuntu', {'random_seed': '-so-was-josh'})
339+ cc_seed_random.handle('test', cfg, c, LOG, [])
340+ contents = util.load_file(self._seed_file)
341+ self.assertEquals('tiny-tim-was-here-so-was-josh', contents)