summaryrefslogtreecommitdiff
path: root/cloudinit
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit')
-rw-r--r--cloudinit/config/cc_set_passwords.py2
-rw-r--r--cloudinit/handlers/__init__.py7
-rw-r--r--cloudinit/helpers.py5
-rw-r--r--cloudinit/stages.py132
-rw-r--r--cloudinit/user_data.py100
-rw-r--r--cloudinit/version.py2
6 files changed, 165 insertions, 83 deletions
diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
index e93c8c6f..56a36906 100644
--- a/cloudinit/config/cc_set_passwords.py
+++ b/cloudinit/config/cc_set_passwords.py
@@ -75,7 +75,7 @@ def handle(_name, cfg, cloud, log, args):
plist_in.append("%s:%s" % (u, p))
users.append(u)
- ch_in = '\n'.join(plist_in)
+ ch_in = '\n'.join(plist_in) + '\n'
try:
log.debug("Changing password for %s:", users)
util.subp(['chpasswd'], ch_in)
diff --git a/cloudinit/handlers/__init__.py b/cloudinit/handlers/__init__.py
index 497d68c5..1d450061 100644
--- a/cloudinit/handlers/__init__.py
+++ b/cloudinit/handlers/__init__.py
@@ -151,10 +151,9 @@ def walker_handle_handler(pdata, _ctype, _filename, payload):
try:
mod = fixup_handler(importer.import_module(modname))
call_begin(mod, pdata['data'], frequency)
- # Only register and increment
- # after the above have worked (so we don't if it
- # fails)
- handlers.register(mod)
+ # Only register and increment after the above have worked, so we don't
+ # register if it fails starting.
+ handlers.register(mod, initialized=True)
pdata['handlercount'] = curcount + 1
except:
util.logexc(LOG, "Failed at registering python file: %s (part "
diff --git a/cloudinit/helpers.py b/cloudinit/helpers.py
index b91c1290..1c46efde 100644
--- a/cloudinit/helpers.py
+++ b/cloudinit/helpers.py
@@ -281,6 +281,7 @@ class ContentHandlers(object):
def __init__(self):
self.registered = {}
+ self.initialized = []
def __contains__(self, item):
return self.is_registered(item)
@@ -291,11 +292,13 @@ class ContentHandlers(object):
def is_registered(self, content_type):
return content_type in self.registered
- def register(self, mod):
+ def register(self, mod, initialized=False):
types = set()
for t in mod.list_types():
self.registered[t] = mod
types.add(t)
+ if initialized and mod not in self.initialized:
+ self.initialized.append(mod)
return types
def _get_handler(self, content_type):
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index df49cabb..3e49e8c5 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -344,12 +344,13 @@ class Init(object):
cdir = self.paths.get_cpath("handlers")
idir = self._get_ipath("handlers")
- # Add the path to the plugins dir to the top of our list for import
- # instance dir should be read before cloud-dir
- if cdir and cdir not in sys.path:
- sys.path.insert(0, cdir)
- if idir and idir not in sys.path:
- sys.path.insert(0, idir)
+ # Add the path to the plugins dir to the top of our list for importing
+ # new handlers.
+ #
+ # Note(harlowja): instance dir should be read before cloud-dir
+ for d in [cdir, idir]:
+ if d and d not in sys.path:
+ sys.path.insert(0, d)
# Ensure datasource fetched before activation (just incase)
user_data_msg = self.datasource.get_userdata(True)
@@ -357,24 +358,34 @@ class Init(object):
# This keeps track of all the active handlers
c_handlers = helpers.ContentHandlers()
- # Add handlers in cdir
- potential_handlers = util.find_modules(cdir)
- for (fname, mod_name) in potential_handlers.iteritems():
- try:
- mod_locs = importer.find_module(mod_name, [''],
- ['list_types',
- 'handle_part'])
- if not mod_locs:
- LOG.warn(("Could not find a valid user-data handler"
- " named %s in file %s"), mod_name, fname)
- continue
- mod = importer.import_module(mod_locs[0])
- mod = handlers.fixup_handler(mod)
- types = c_handlers.register(mod)
- LOG.debug("Added handler for %s from %s", types, fname)
- except:
- util.logexc(LOG, "Failed to register handler from %s", fname)
-
+ def register_handlers_in_dir(path):
+ # Attempts to register any handler modules under the given path.
+ if not path or not os.path.isdir(path):
+ return
+ potential_handlers = util.find_modules(path)
+ for (fname, mod_name) in potential_handlers.iteritems():
+ try:
+ mod_locs = importer.find_module(mod_name, [''],
+ ['list_types',
+ 'handle_part'])
+ if not mod_locs:
+ LOG.warn(("Could not find a valid user-data handler"
+ " named %s in file %s"), mod_name, fname)
+ continue
+ mod = importer.import_module(mod_locs[0])
+ mod = handlers.fixup_handler(mod)
+ types = c_handlers.register(mod)
+ LOG.debug("Added handler for %s from %s", types, fname)
+ except Exception:
+ util.logexc(LOG, "Failed to register handler from %s",
+ fname)
+
+ # Add any handlers in the cloud-dir
+ register_handlers_in_dir(cdir)
+
+ # Register any other handlers that come from the default set. This
+ # is done after the cloud-dir handlers so that the cdir modules can
+ # take over the default user-data handler content-types.
def_handlers = self._default_userdata_handlers()
applied_def_handlers = c_handlers.register_defaults(def_handlers)
if applied_def_handlers:
@@ -383,36 +394,51 @@ class Init(object):
# Form our cloud interface
data = self.cloudify()
- # Init the handlers first
- called = []
- for (_ctype, mod) in c_handlers.iteritems():
- if mod in called:
- continue
- handlers.call_begin(mod, data, frequency)
- called.append(mod)
-
- # Walk the user data
- part_data = {
- 'handlers': c_handlers,
- # Any new handlers that are encountered get writen here
- 'handlerdir': idir,
- 'data': data,
- # The default frequency if handlers don't have one
- 'frequency': frequency,
- # This will be used when new handlers are found
- # to help write there contents to files with numbered
- # names...
- 'handlercount': 0,
- }
- handlers.walk(user_data_msg, handlers.walker_callback, data=part_data)
+ def init_handlers():
+ # Init the handlers first
+ for (_ctype, mod) in c_handlers.iteritems():
+ if mod in c_handlers.initialized:
+ # Avoid initing the same module twice (if said module
+ # is registered to more than one content-type).
+ continue
+ handlers.call_begin(mod, data, frequency)
+ c_handlers.initialized.append(mod)
+
+ def walk_handlers():
+ # Walk the user data
+ part_data = {
+ 'handlers': c_handlers,
+ # Any new handlers that are encountered get writen here
+ 'handlerdir': idir,
+ 'data': data,
+ # The default frequency if handlers don't have one
+ 'frequency': frequency,
+ # This will be used when new handlers are found
+ # to help write there contents to files with numbered
+ # names...
+ 'handlercount': 0,
+ }
+ handlers.walk(user_data_msg, handlers.walker_callback,
+ data=part_data)
+
+ def finalize_handlers():
+ # Give callbacks opportunity to finalize
+ for (_ctype, mod) in c_handlers.iteritems():
+ if mod not in c_handlers.initialized:
+ # Said module was never inited in the first place, so lets
+ # not attempt to finalize those that never got called.
+ continue
+ c_handlers.initialized.remove(mod)
+ try:
+ handlers.call_end(mod, data, frequency)
+ except:
+ util.logexc(LOG, "Failed to finalize handler: %s", mod)
- # Give callbacks opportunity to finalize
- called = []
- for (_ctype, mod) in c_handlers.iteritems():
- if mod in called:
- continue
- handlers.call_end(mod, data, frequency)
- called.append(mod)
+ try:
+ init_handlers()
+ walk_handlers()
+ finally:
+ finalize_handlers()
# Perform post-consumption adjustments so that
# modules that run during the init stage reflect
diff --git a/cloudinit/user_data.py b/cloudinit/user_data.py
index df069ff8..d49ea094 100644
--- a/cloudinit/user_data.py
+++ b/cloudinit/user_data.py
@@ -23,8 +23,10 @@
import os
import email
+
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
+from email.mime.nonmultipart import MIMENonMultipart
from email.mime.text import MIMEText
from cloudinit import handlers
@@ -48,6 +50,18 @@ ARCHIVE_TYPES = ["text/cloud-config-archive"]
UNDEF_TYPE = "text/plain"
ARCHIVE_UNDEF_TYPE = "text/cloud-config"
+# This seems to hit most of the gzip possible content types.
+DECOMP_TYPES = [
+ 'application/gzip',
+ 'application/gzip-compressed',
+ 'application/gzipped',
+ 'application/x-compress',
+ 'application/x-compressed',
+ 'application/x-gunzip',
+ 'application/x-gzip',
+ 'application/x-gzip-compressed',
+]
+
# Msg header used to track attachments
ATTACHMENT_FIELD = 'Number-Attachments'
@@ -56,6 +70,17 @@ ATTACHMENT_FIELD = 'Number-Attachments'
EXAMINE_FOR_LAUNCH_INDEX = ["text/cloud-config"]
+def _replace_header(msg, key, value):
+ del msg[key]
+ msg[key] = value
+
+
+def _set_filename(msg, filename):
+ del msg['Content-Disposition']
+ msg.add_header('Content-Disposition',
+ 'attachment', filename=str(filename))
+
+
class UserDataProcessor(object):
def __init__(self, paths):
self.paths = paths
@@ -67,6 +92,10 @@ class UserDataProcessor(object):
return accumulating_msg
def _process_msg(self, base_msg, append_msg):
+
+ def find_ctype(payload):
+ return handlers.type_from_starts_with(payload)
+
for part in base_msg.walk():
if is_skippable(part):
continue
@@ -74,21 +103,51 @@ class UserDataProcessor(object):
ctype = None
ctype_orig = part.get_content_type()
payload = part.get_payload(decode=True)
+ was_compressed = False
+
+ # When the message states it is of a gzipped content type ensure
+ # that we attempt to decode said payload so that the decompressed
+ # data can be examined (instead of the compressed data).
+ if ctype_orig in DECOMP_TYPES:
+ try:
+ payload = util.decomp_gzip(payload, quiet=False)
+ # At this point we don't know what the content-type is
+ # since we just decompressed it.
+ ctype_orig = None
+ was_compressed = True
+ except util.DecompressionError as e:
+ LOG.warn("Failed decompressing payload from %s of length"
+ " %s due to: %s", ctype_orig, len(payload), e)
+ continue
+ # Attempt to figure out the payloads content-type
if not ctype_orig:
ctype_orig = UNDEF_TYPE
-
if ctype_orig in TYPE_NEEDED:
- ctype = handlers.type_from_starts_with(payload)
-
+ ctype = find_ctype(payload)
if ctype is None:
ctype = ctype_orig
+ # In the case where the data was compressed, we want to make sure
+ # that we create a new message that contains the found content
+ # type with the uncompressed content since later traversals of the
+ # messages will expect a part not compressed.
+ if was_compressed:
+ maintype, subtype = ctype.split("/", 1)
+ n_part = MIMENonMultipart(maintype, subtype)
+ n_part.set_payload(payload)
+ # Copy various headers from the old part to the new one,
+ # but don't include all the headers since some are not useful
+ # after decoding and decompression.
+ if part.get_filename():
+ _set_filename(n_part, part.get_filename())
+ for h in ('Launch-Index',):
+ if h in part:
+ _replace_header(n_part, h, str(part[h]))
+ part = n_part
+
if ctype != ctype_orig:
- if CONTENT_TYPE in part:
- part.replace_header(CONTENT_TYPE, ctype)
- else:
- part[CONTENT_TYPE] = ctype
+ _replace_header(part, CONTENT_TYPE, ctype)
if ctype in INCLUDE_TYPES:
self._do_include(payload, append_msg)
@@ -98,12 +157,9 @@ class UserDataProcessor(object):
self._explode_archive(payload, append_msg)
continue
- # Should this be happening, shouldn't
+ # TODO(harlowja): Should this be happening, shouldn't
# the part header be modified and not the base?
- if CONTENT_TYPE in base_msg:
- base_msg.replace_header(CONTENT_TYPE, ctype)
- else:
- base_msg[CONTENT_TYPE] = ctype
+ _replace_header(base_msg, CONTENT_TYPE, ctype)
self._attach_part(append_msg, part)
@@ -138,8 +194,7 @@ class UserDataProcessor(object):
def _process_before_attach(self, msg, attached_id):
if not msg.get_filename():
- msg.add_header('Content-Disposition',
- 'attachment', filename=PART_FN_TPL % (attached_id))
+ _set_filename(msg, PART_FN_TPL % (attached_id))
self._attach_launch_index(msg)
def _do_include(self, content, append_msg):
@@ -217,13 +272,15 @@ class UserDataProcessor(object):
msg.set_payload(content)
if 'filename' in ent:
- msg.add_header('Content-Disposition',
- 'attachment', filename=ent['filename'])
+ _set_filename(msg, ent['filename'])
if 'launch-index' in ent:
msg.add_header('Launch-Index', str(ent['launch-index']))
for header in list(ent.keys()):
- if header in ('content', 'filename', 'type', 'launch-index'):
+ if header.lower() in ('content', 'filename', 'type',
+ 'launch-index', 'content-disposition',
+ ATTACHMENT_FIELD.lower(),
+ CONTENT_TYPE.lower()):
continue
msg.add_header(header, ent[header])
@@ -238,13 +295,13 @@ class UserDataProcessor(object):
outer_msg[ATTACHMENT_FIELD] = '0'
if new_count is not None:
- outer_msg.replace_header(ATTACHMENT_FIELD, str(new_count))
+ _replace_header(outer_msg, ATTACHMENT_FIELD, str(new_count))
fetched_count = 0
try:
fetched_count = int(outer_msg.get(ATTACHMENT_FIELD))
except (ValueError, TypeError):
- outer_msg.replace_header(ATTACHMENT_FIELD, str(fetched_count))
+ _replace_header(outer_msg, ATTACHMENT_FIELD, str(fetched_count))
return fetched_count
def _attach_part(self, outer_msg, part):
@@ -276,10 +333,7 @@ def convert_string(raw_data, headers=None):
if "mime-version:" in data[0:4096].lower():
msg = email.message_from_string(data)
for (key, val) in headers.iteritems():
- if key in msg:
- msg.replace_header(key, val)
- else:
- msg[key] = val
+ _replace_header(msg, key, val)
else:
mtype = headers.get(CONTENT_TYPE, NOT_MULTIPART_TYPE)
maintype, subtype = mtype.split("/", 1)
diff --git a/cloudinit/version.py b/cloudinit/version.py
index 024d5118..4b29a587 100644
--- a/cloudinit/version.py
+++ b/cloudinit/version.py
@@ -20,7 +20,7 @@ from distutils import version as vr
def version():
- return vr.StrictVersion("0.7.2")
+ return vr.StrictVersion("0.7.3")
def version_string():