3
# Copyright (C) 2012 Canonical Ltd.
4
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
5
# Copyright (C) 2012 Yahoo! Inc.
7
# Author: Scott Moser <scott.moser@canonical.com>
8
# Author: Juerg Haefliger <juerg.haefliger@hp.com>
9
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
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.
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.
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/>.
26
from email.mime.multipart import MIMEMultipart
27
from email.mime.text import MIMEText
28
from email.mime.base import MIMEBase
30
from cloudinit import handlers
31
from cloudinit import log as logging
32
from cloudinit import url_helper
33
from cloudinit import util
35
LOG = logging.getLogger(__name__)
37
# Constants copied in from the handler module
38
NOT_MULTIPART_TYPE = handlers.NOT_MULTIPART_TYPE
39
PART_FN_TPL = handlers.PART_FN_TPL
40
OCTET_TYPE = handlers.OCTET_TYPE
43
CONTENT_TYPE = 'Content-Type'
45
# Various special content types that cause special actions
46
TYPE_NEEDED = ["text/plain", "text/x-not-multipart"]
47
INCLUDE_TYPES = ['text/x-include-url', 'text/x-include-once-url']
48
ARCHIVE_TYPES = ["text/cloud-config-archive"]
49
UNDEF_TYPE = "text/plain"
50
ARCHIVE_UNDEF_TYPE = "text/cloud-config"
52
# Msg header used to track attachments
53
ATTACHMENT_FIELD = 'Number-Attachments'
56
class UserDataProcessor(object):
57
def __init__(self, paths):
60
def process(self, blob):
61
base_msg = convert_string(blob)
62
process_msg = MIMEMultipart()
63
self._process_msg(base_msg, process_msg)
66
def _process_msg(self, base_msg, append_msg):
67
for part in base_msg.walk():
68
# multipart/* are just containers
69
if part.get_content_maintype() == 'multipart':
73
ctype_orig = part.get_content_type()
74
payload = part.get_payload(decode=True)
77
ctype_orig = UNDEF_TYPE
79
if ctype_orig in TYPE_NEEDED:
80
ctype = handlers.type_from_starts_with(payload)
85
if ctype in INCLUDE_TYPES:
86
self._do_include(payload, append_msg)
89
if ctype in ARCHIVE_TYPES:
90
self._explode_archive(payload, append_msg)
93
if CONTENT_TYPE in base_msg:
94
base_msg.replace_header(CONTENT_TYPE, ctype)
96
base_msg[CONTENT_TYPE] = ctype
98
self._attach_part(append_msg, part)
100
def _get_include_once_filename(self, entry):
101
entry_fn = util.hash_blob(entry, 'md5', 64)
102
return os.path.join(self.paths.get_ipath_cur('data'),
103
'urlcache', entry_fn)
105
def _do_include(self, content, append_msg):
106
# Include a list of urls, one per line
107
# also support '#include <url here>'
108
# or #include-once '<url here>'
109
include_once_on = False
110
for line in content.splitlines():
111
lc_line = line.lower()
112
if lc_line.startswith("#include-once"):
113
line = line[len("#include-once"):].lstrip()
114
# Every following include will now
115
# not be refetched.... but will be
116
# re-read from a local urlcache (if it worked)
117
include_once_on = True
118
elif lc_line.startswith("#include"):
119
line = line[len("#include"):].lstrip()
120
# Disable the include once if it was on
121
# if it wasn't, then this has no effect.
122
include_once_on = False
123
if line.startswith("#"):
125
include_url = line.strip()
129
include_once_fn = None
132
include_once_fn = self._get_include_once_filename(include_url)
133
if include_once_on and os.path.isfile(include_once_fn):
134
content = util.load_file(include_once_fn)
136
resp = url_helper.readurl(include_url)
137
if include_once_on and resp.ok():
138
util.write_file(include_once_fn, str(resp), mode=0600)
142
LOG.warn(("Fetching from %s resulted in"
143
" a invalid http code of %s"),
144
include_url, resp.code)
146
if content is not None:
147
new_msg = convert_string(content)
148
self._process_msg(new_msg, append_msg)
150
def _explode_archive(self, archive, append_msg):
151
entries = util.load_yaml(archive, default=[], allowed=[list, set])
154
# dict { 'filename' : 'value', 'content' :
155
# 'value', 'type' : 'value' }
156
# filename and type not be present
159
if isinstance(ent, (str, basestring)):
160
ent = {'content': ent}
161
if not isinstance(ent, (dict)):
165
content = ent.get('content', '')
166
mtype = ent.get('type')
168
mtype = handlers.type_from_starts_with(content,
171
maintype, subtype = mtype.split('/', 1)
172
if maintype == "text":
173
msg = MIMEText(content, _subtype=subtype)
175
msg = MIMEBase(maintype, subtype)
176
msg.set_payload(content)
178
if 'filename' in ent:
179
msg.add_header('Content-Disposition',
180
'attachment', filename=ent['filename'])
182
for header in list(ent.keys()):
183
if header in ('content', 'filename', 'type'):
185
msg.add_header(header, ent['header'])
187
self._attach_part(append_msg, msg)
189
def _multi_part_count(self, outer_msg, new_count=None):
191
Return the number of attachments to this MIMEMultipart by looking
192
at its 'Number-Attachments' header.
194
if ATTACHMENT_FIELD not in outer_msg:
195
outer_msg[ATTACHMENT_FIELD] = '0'
197
if new_count is not None:
198
outer_msg.replace_header(ATTACHMENT_FIELD, str(new_count))
202
fetched_count = int(outer_msg.get(ATTACHMENT_FIELD))
203
except (ValueError, TypeError):
204
outer_msg.replace_header(ATTACHMENT_FIELD, str(fetched_count))
207
def _part_filename(self, _unnamed_part, count):
208
return PART_FN_TPL % (count + 1)
210
def _attach_part(self, outer_msg, part):
212
Attach an part to an outer message. outermsg must be a MIMEMultipart.
213
Modifies a header in the message to keep track of number of attachments.
215
cur_c = self._multi_part_count(outer_msg)
216
if not part.get_filename():
217
fn = self._part_filename(part, cur_c)
218
part.add_header('Content-Disposition',
219
'attachment', filename=fn)
220
outer_msg.attach(part)
221
self._multi_part_count(outer_msg, cur_c + 1)
224
# Coverts a raw string into a mime message
225
def convert_string(raw_data, headers=None):
230
data = util.decomp_str(raw_data)
231
if "mime-version:" in data[0:4096].lower():
232
msg = email.message_from_string(data)
233
for (key, val) in headers.iteritems():
235
msg.replace_header(key, val)
239
mtype = headers.get(CONTENT_TYPE, NOT_MULTIPART_TYPE)
240
maintype, subtype = mtype.split("/", 1)
241
msg = MIMEBase(maintype, subtype, *headers)
242
msg.set_payload(data)