summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog2
-rw-r--r--cloudinit/UserDataHandler.py170
2 files changed, 99 insertions, 73 deletions
diff --git a/ChangeLog b/ChangeLog
index 552e1023..3dad0f09 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -15,6 +15,8 @@
- add 'cloud-init-per' script for easily running things with a given frequency
- replace cloud-init-run-module with cloud-init-per
- support configuration of landscape-client via cloud-config (LP: #857366)
+ - part-handlers now get base64 decoded content rather than 2xbase64 encoded
+ in the payload parameter. (LP: #874342)
0.6.2:
- fix bug where update was not done unless update was explicitly set.
It would not be run if 'upgrade' or packages were set to be installed
diff --git a/cloudinit/UserDataHandler.py b/cloudinit/UserDataHandler.py
index 982e57f0..541ee87a 100644
--- a/cloudinit/UserDataHandler.py
+++ b/cloudinit/UserDataHandler.py
@@ -25,6 +25,8 @@ import yaml
import cloudinit
import cloudinit.util as util
import hashlib
+import os
+import urllib
starts_with_mappings={
'#include' : 'text/x-include-url',
@@ -47,13 +49,12 @@ def decomp_str(str):
except:
return(str)
-def do_include(str,parts):
- import urllib
+def do_include(content, appendmsg):
import os
# is just a list of urls, one per line
# also support '#include <url here>'
includeonce = False
- for line in str.splitlines():
+ for line in content.splitlines():
if line == "#include": continue
if line == "#include-once":
includeonce = True
@@ -81,31 +82,70 @@ def do_include(str,parts):
except Exception as e:
raise
- process_includes(email.message_from_string(decomp_str(content)),parts)
+ process_includes(message_from_string(decomp_str(content)), appendmsg)
-def explode_cc_archive(archive,parts):
+def explode_cc_archive(archive, appendmsg):
for ent in yaml.load(archive):
# ent can be one of:
# dict { 'filename' : 'value' , 'content' : 'value', 'type' : 'value' }
# filename and type not be present
# or
# scalar(payload)
- filename = 'part-%03d' % len(parts['content'])
+
def_type = "text/cloud-config"
if isinstance(ent,str):
- content = ent
- mtype = type_from_startswith(content,def_type)
+ ent = { 'content': ent }
+
+ content = ent.get('content', '')
+ mtype = ent.get('type', None)
+ if mtype == None:
+ mtype = type_from_startswith(content, def_type)
+
+ maintype, subtype = mtype.split('/', 1)
+ if maintype == "text":
+ msg = MIMEText(content, _subtype=subtype)
else:
- content = ent.get('content', '')
- filename = ent.get('filename', filename)
- mtype = ent.get('type', None)
- if mtype == None:
- mtype = type_from_startswith(payload,def_type)
-
- parts['content'].append(content)
- parts['names'].append(filename)
- parts['types'].append(mtype)
+ msg = MIMEBase(maintype, subtype)
+ msg.set_payload(content)
+
+ if 'filename' in ent:
+ msg.add_header('Content-Disposition', 'attachment',
+ filename=ent['filename'])
+
+ for header in ent.keys():
+ if header in ('content', 'filename', 'type'):
+ continue
+ msg.add_header(header, ent['header'])
+
+ _attach_part(appendmsg,msg)
+
+
+def multi_part_count(outermsg, newcount=None):
+ """
+ Return the number of attachments to this MIMEMultipart by looking
+ at its 'Number-Attachments' header.
+ """
+ nfield = 'Number-Attachments'
+ if nfield not in outermsg:
+ outermsg[nfield] = "0"
+
+ if newcount != None:
+ outermsg.replace_header(nfield, str(newcount))
+
+ return(int(outermsg.get('Number-Attachments', 0)))
+
+def _attach_part(outermsg, part):
+ """
+ Attach an part to an outer message. outermsg must be a MIMEMultipart.
+ Modifies a header in outermsg to keep track of number of attachments.
+ """
+ cur = multi_part_count(outermsg)
+ if not part.get_filename(None):
+ part.add_header('Content-Disposition', 'attachment',
+ filename = 'part-%03d' % (cur+1))
+ outermsg.attach(part)
+ multi_part_count(outermsg, cur+1)
def type_from_startswith(payload, default=None):
# slist is sorted longest first
@@ -115,85 +155,69 @@ def type_from_startswith(payload, default=None):
return(starts_with_mappings[sstr])
return default
-def process_includes(msg,parts):
- # parts is a dictionary of arrays
- # parts['content']
- # parts['names']
- # parts['types']
- for t in ( 'content', 'names', 'types' ):
- if not parts.has_key(t):
- parts[t]=[ ]
+def process_includes(msg, appendmsg=None):
+ if appendmsg == None:
+ appendmsg = MIMEMultipart()
+
for part in msg.walk():
# multipart/* are just containers
if part.get_content_maintype() == 'multipart':
continue
- payload = part.get_payload()
-
ctype = None
ctype_orig = part.get_content_type()
+
+ payload = part.get_payload(decode=True)
+
if ctype_orig == "text/plain":
ctype = type_from_startswith(payload)
if ctype is None:
ctype = ctype_orig
- if ctype == 'text/x-include-url':
- do_include(payload,parts)
- continue
-
- if ctype == 'text/x-include-once-url':
- do_include(payload,parts)
+ if ctype in ('text/x-include-url', 'text/x-include-once-url'):
+ do_include(payload, appendmsg)
continue
if ctype == "text/cloud-config-archive":
- explode_cc_archive(payload,parts)
+ explode_cc_archive(payload, appendmsg)
continue
- filename = part.get_filename()
- if not filename:
- filename = 'part-%03d' % len(parts['content'])
-
- parts['content'].append(payload)
- parts['types'].append(ctype)
- parts['names'].append(filename)
+ if 'Content-Type' in msg:
+ msg.replace_header('Content-Type', ctype)
+ else:
+ msg['Content-Type'] = ctype
-def parts2mime(parts):
- outer = MIMEMultipart()
+ _attach_part(appendmsg, part)
- i = 0
- while i < len(parts['content']):
- if parts['types'][i] is None:
- # No guess could be made, or the file is encoded (compressed), so
- # use a generic bag-of-bits type.
- ctype = 'application/octet-stream'
- else: ctype = parts['types'][i]
- maintype, subtype = ctype.split('/', 1)
- if maintype == 'text':
- msg = MIMEText(parts['content'][i], _subtype=subtype)
- else:
- msg = MIMEBase(maintype, subtype)
- msg.set_payload(parts['content'][i])
- # Encode the payload using Base64
- encoders.encode_base64(msg)
- # Set the filename parameter
- msg.add_header('Content-Disposition', 'attachment',
- filename=parts['names'][i])
- outer.attach(msg)
+def message_from_string(data, headers={}):
+ if "mime-version:" in data[0:4096].lower():
+ was_mime = True
+ msg = email.message_from_string(data)
+ for (key,val) in headers.items():
+ if key in msg:
+ msg.replace_header(key,val)
+ else:
+ msg[key] = val
+ else:
+ was_mime = False
+ mtype = headers.get("Content-Type","text/plain")
+ maintype, subtype = mtype.split("/", 1)
+ msg = MIMEBase(maintype, subtype, *headers)
+ msg.set_payload(data)
- i=i+1
- return(outer.as_string())
+ return(msg)
# this is heavily wasteful, reads through userdata string input
def preprocess_userdata(data):
- parts = { }
- process_includes(email.message_from_string(decomp_str(data)),parts)
- return(parts2mime(parts))
+ newmsg = MIMEMultipart()
+ process_includes(message_from_string(decomp_str(data)), newmsg)
+ return(newmsg.as_string())
# callback is a function that will be called with (data, content_type, filename, payload)
def walk_userdata(istr, callback, data = None):
partnum = 0
- for part in email.message_from_string(istr).walk():
+ for part in message_from_string(istr).walk():
# multipart/* are just containers
if part.get_content_maintype() == 'multipart':
continue
@@ -206,14 +230,14 @@ def walk_userdata(istr, callback, data = None):
if not filename:
filename = 'part-%03d' % partnum
- callback(data, ctype, filename, part.get_payload())
+ callback(data, ctype, filename, part.get_payload(decode=True))
partnum = partnum+1
if __name__ == "__main__":
import sys
data = decomp_str(file(sys.argv[1]).read())
- parts = { }
- process_includes(email.message_from_string(data),parts)
- print "#found %s parts" % len(parts['content'])
- print parts2mime(parts)
+ newmsg = MIMEMultipart()
+ process_includes(message_from_string(data), newmsg)
+ print newmsg
+ print "#found %s parts" % multi_part_count(newmsg)