Blame cloudinit/user_data.py

Packit Service a04d08
# Copyright (C) 2012 Canonical Ltd.
Packit Service a04d08
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
Packit Service a04d08
# Copyright (C) 2012 Yahoo! Inc.
Packit Service a04d08
#
Packit Service a04d08
# Author: Scott Moser <scott.moser@canonical.com>
Packit Service a04d08
# Author: Juerg Haefliger <juerg.haefliger@hp.com>
Packit Service a04d08
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
Packit Service a04d08
#
Packit Service a04d08
# This file is part of cloud-init. See LICENSE file for license information.
Packit Service a04d08
Packit Service a04d08
import os
Packit Service a04d08
from email.mime.base import MIMEBase
Packit Service a04d08
from email.mime.multipart import MIMEMultipart
Packit Service a04d08
from email.mime.nonmultipart import MIMENonMultipart
Packit Service a04d08
from email.mime.text import MIMEText
Packit Service a04d08
Packit Service a04d08
from cloudinit import handlers
Packit Service a04d08
from cloudinit import log as logging
Packit Service 751c4a
from cloudinit import features
Packit Service a04d08
from cloudinit.url_helper import read_file_or_url, UrlError
Packit Service a04d08
from cloudinit import util
Packit Service a04d08
Packit Service a04d08
LOG = logging.getLogger(__name__)
Packit Service a04d08
Packit Service a04d08
# Constants copied in from the handler module
Packit Service a04d08
NOT_MULTIPART_TYPE = handlers.NOT_MULTIPART_TYPE
Packit Service a04d08
PART_FN_TPL = handlers.PART_FN_TPL
Packit Service a04d08
OCTET_TYPE = handlers.OCTET_TYPE
Packit Service 751c4a
INCLUDE_MAP = handlers.INCLUSION_TYPES_MAP
Packit Service a04d08
Packit Service a04d08
# Saves typing errors
Packit Service a04d08
CONTENT_TYPE = 'Content-Type'
Packit Service a04d08
Packit Service a04d08
# Various special content types that cause special actions
Packit Service a04d08
TYPE_NEEDED = ["text/plain", "text/x-not-multipart"]
Packit Service a04d08
INCLUDE_TYPES = ['text/x-include-url', 'text/x-include-once-url']
Packit Service a04d08
ARCHIVE_TYPES = ["text/cloud-config-archive"]
Packit Service a04d08
UNDEF_TYPE = "text/plain"
Packit Service a04d08
ARCHIVE_UNDEF_TYPE = "text/cloud-config"
Packit Service a04d08
ARCHIVE_UNDEF_BINARY_TYPE = "application/octet-stream"
Packit Service a04d08
Packit Service a04d08
# This seems to hit most of the gzip possible content types.
Packit Service a04d08
DECOMP_TYPES = [
Packit Service a04d08
    'application/gzip',
Packit Service a04d08
    'application/gzip-compressed',
Packit Service a04d08
    'application/gzipped',
Packit Service a04d08
    'application/x-compress',
Packit Service a04d08
    'application/x-compressed',
Packit Service a04d08
    'application/x-gunzip',
Packit Service a04d08
    'application/x-gzip',
Packit Service a04d08
    'application/x-gzip-compressed',
Packit Service a04d08
]
Packit Service a04d08
Packit Service a04d08
# Msg header used to track attachments
Packit Service a04d08
ATTACHMENT_FIELD = 'Number-Attachments'
Packit Service a04d08
Packit Service a04d08
# Only the following content types can have there launch index examined
Packit Service a04d08
# in there payload, evey other content type can still provide a header
Packit Service a04d08
EXAMINE_FOR_LAUNCH_INDEX = ["text/cloud-config"]
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def _replace_header(msg, key, value):
Packit Service a04d08
    del msg[key]
Packit Service a04d08
    msg[key] = value
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def _set_filename(msg, filename):
Packit Service a04d08
    del msg['Content-Disposition']
Packit Service a04d08
    msg.add_header('Content-Disposition',
Packit Service a04d08
                   'attachment', filename=str(filename))
Packit Service a04d08
Packit Service a04d08
Packit Service 751c4a
def _handle_error(error_message, source_exception=None):
Packit Service 751c4a
    if features.ERROR_ON_USER_DATA_FAILURE:
Packit Service 751c4a
        raise Exception(error_message) from source_exception
Packit Service 751c4a
    else:
Packit Service 751c4a
        LOG.warning(error_message)
Packit Service 751c4a
Packit Service 751c4a
Packit Service a04d08
class UserDataProcessor(object):
Packit Service a04d08
    def __init__(self, paths):
Packit Service a04d08
        self.paths = paths
Packit Service a04d08
        self.ssl_details = util.fetch_ssl_details(paths)
Packit Service a04d08
Packit Service a04d08
    def process(self, blob):
Packit Service a04d08
        accumulating_msg = MIMEMultipart()
Packit Service a04d08
        if isinstance(blob, list):
Packit Service a04d08
            for b in blob:
Packit Service a04d08
                self._process_msg(convert_string(b), accumulating_msg)
Packit Service a04d08
        else:
Packit Service a04d08
            self._process_msg(convert_string(blob), accumulating_msg)
Packit Service a04d08
        return accumulating_msg
Packit Service a04d08
Packit Service a04d08
    def _process_msg(self, base_msg, append_msg):
Packit Service a04d08
Packit Service a04d08
        def find_ctype(payload):
Packit Service a04d08
            return handlers.type_from_starts_with(payload)
Packit Service a04d08
Packit Service a04d08
        for part in base_msg.walk():
Packit Service a04d08
            if is_skippable(part):
Packit Service a04d08
                continue
Packit Service a04d08
Packit Service a04d08
            ctype = None
Packit Service a04d08
            ctype_orig = part.get_content_type()
Packit Service a04d08
            payload = util.fully_decoded_payload(part)
Packit Service a04d08
            was_compressed = False
Packit Service a04d08
Packit Service a04d08
            # When the message states it is of a gzipped content type ensure
Packit Service a04d08
            # that we attempt to decode said payload so that the decompressed
Packit Service a04d08
            # data can be examined (instead of the compressed data).
Packit Service a04d08
            if ctype_orig in DECOMP_TYPES:
Packit Service a04d08
                try:
Packit Service a04d08
                    payload = util.decomp_gzip(payload, quiet=False)
Packit Service a04d08
                    # At this point we don't know what the content-type is
Packit Service a04d08
                    # since we just decompressed it.
Packit Service a04d08
                    ctype_orig = None
Packit Service a04d08
                    was_compressed = True
Packit Service a04d08
                except util.DecompressionError as e:
Packit Service 751c4a
                    error_message = (
Packit Service 751c4a
                        "Failed decompressing payload from {} of"
Packit Service 751c4a
                        " length {} due to: {}".format(
Packit Service 751c4a
                            ctype_orig, len(payload), e))
Packit Service 751c4a
                    _handle_error(error_message, e)
Packit Service a04d08
                    continue
Packit Service a04d08
Packit Service a04d08
            # Attempt to figure out the payloads content-type
Packit Service a04d08
            if not ctype_orig:
Packit Service a04d08
                ctype_orig = UNDEF_TYPE
Packit Service 751c4a
            # There are known cases where mime-type text/x-shellscript included
Packit Service 751c4a
            # non shell-script content that was user-data instead.  It is safe
Packit Service 751c4a
            # to check the true MIME type for x-shellscript type since all
Packit Service 751c4a
            # shellscript payloads must have a #! header.  The other MIME types
Packit Service 751c4a
            # that cloud-init supports do not have the same guarantee.
Packit Service 751c4a
            if ctype_orig in TYPE_NEEDED + ['text/x-shellscript']:
Packit Service a04d08
                ctype = find_ctype(payload)
Packit Service a04d08
            if ctype is None:
Packit Service a04d08
                ctype = ctype_orig
Packit Service a04d08
Packit Service a04d08
            # In the case where the data was compressed, we want to make sure
Packit Service a04d08
            # that we create a new message that contains the found content
Packit Service a04d08
            # type with the uncompressed content since later traversals of the
Packit Service a04d08
            # messages will expect a part not compressed.
Packit Service a04d08
            if was_compressed:
Packit Service a04d08
                maintype, subtype = ctype.split("/", 1)
Packit Service a04d08
                n_part = MIMENonMultipart(maintype, subtype)
Packit Service a04d08
                n_part.set_payload(payload)
Packit Service a04d08
                # Copy various headers from the old part to the new one,
Packit Service a04d08
                # but don't include all the headers since some are not useful
Packit Service a04d08
                # after decoding and decompression.
Packit Service a04d08
                if part.get_filename():
Packit Service a04d08
                    _set_filename(n_part, part.get_filename())
Packit Service a04d08
                for h in ('Launch-Index',):
Packit Service a04d08
                    if h in part:
Packit Service a04d08
                        _replace_header(n_part, h, str(part[h]))
Packit Service a04d08
                part = n_part
Packit Service a04d08
Packit Service a04d08
            if ctype != ctype_orig:
Packit Service a04d08
                _replace_header(part, CONTENT_TYPE, ctype)
Packit Service a04d08
Packit Service a04d08
            if ctype in INCLUDE_TYPES:
Packit Service a04d08
                self._do_include(payload, append_msg)
Packit Service a04d08
                continue
Packit Service a04d08
Packit Service a04d08
            if ctype in ARCHIVE_TYPES:
Packit Service a04d08
                self._explode_archive(payload, append_msg)
Packit Service a04d08
                continue
Packit Service a04d08
Packit Service a04d08
            # TODO(harlowja): Should this be happening, shouldn't
Packit Service a04d08
            # the part header be modified and not the base?
Packit Service a04d08
            _replace_header(base_msg, CONTENT_TYPE, ctype)
Packit Service a04d08
Packit Service a04d08
            self._attach_part(append_msg, part)
Packit Service a04d08
Packit Service a04d08
    def _attach_launch_index(self, msg):
Packit Service a04d08
        header_idx = msg.get('Launch-Index', None)
Packit Service a04d08
        payload_idx = None
Packit Service a04d08
        if msg.get_content_type() in EXAMINE_FOR_LAUNCH_INDEX:
Packit Service a04d08
            try:
Packit Service a04d08
                # See if it has a launch-index field
Packit Service a04d08
                # that might affect the final header
Packit Service a04d08
                payload = util.load_yaml(msg.get_payload(decode=True))
Packit Service a04d08
                if payload:
Packit Service a04d08
                    payload_idx = payload.get('launch-index')
Packit Service a04d08
            except Exception:
Packit Service a04d08
                pass
Packit Service a04d08
        # Header overrides contents, for now (?) or the other way around?
Packit Service a04d08
        if header_idx is not None:
Packit Service a04d08
            payload_idx = header_idx
Packit Service a04d08
        # Nothing found in payload, use header (if anything there)
Packit Service a04d08
        if payload_idx is None:
Packit Service a04d08
            payload_idx = header_idx
Packit Service a04d08
        if payload_idx is not None:
Packit Service a04d08
            try:
Packit Service a04d08
                msg.add_header('Launch-Index', str(int(payload_idx)))
Packit Service a04d08
            except (ValueError, TypeError):
Packit Service a04d08
                pass
Packit Service a04d08
Packit Service a04d08
    def _get_include_once_filename(self, entry):
Packit Service a04d08
        entry_fn = util.hash_blob(entry, 'md5', 64)
Packit Service a04d08
        return os.path.join(self.paths.get_ipath_cur('data'),
Packit Service a04d08
                            'urlcache', entry_fn)
Packit Service a04d08
Packit Service a04d08
    def _process_before_attach(self, msg, attached_id):
Packit Service a04d08
        if not msg.get_filename():
Packit Service a04d08
            _set_filename(msg, PART_FN_TPL % (attached_id))
Packit Service a04d08
        self._attach_launch_index(msg)
Packit Service a04d08
Packit Service a04d08
    def _do_include(self, content, append_msg):
Packit Service a04d08
        # Include a list of urls, one per line
Packit Service a04d08
        # also support '#include <url here>'
Packit Service a04d08
        # or #include-once '<url here>'
Packit Service a04d08
        include_once_on = False
Packit Service a04d08
        for line in content.splitlines():
Packit Service a04d08
            lc_line = line.lower()
Packit Service a04d08
            if lc_line.startswith("#include-once"):
Packit Service a04d08
                line = line[len("#include-once"):].lstrip()
Packit Service a04d08
                # Every following include will now
Packit Service a04d08
                # not be refetched.... but will be
Packit Service a04d08
                # re-read from a local urlcache (if it worked)
Packit Service a04d08
                include_once_on = True
Packit Service a04d08
            elif lc_line.startswith("#include"):
Packit Service a04d08
                line = line[len("#include"):].lstrip()
Packit Service a04d08
                # Disable the include once if it was on
Packit Service a04d08
                # if it wasn't, then this has no effect.
Packit Service a04d08
                include_once_on = False
Packit Service a04d08
            if line.startswith("#"):
Packit Service a04d08
                continue
Packit Service a04d08
            include_url = line.strip()
Packit Service a04d08
            if not include_url:
Packit Service a04d08
                continue
Packit Service a04d08
Packit Service a04d08
            include_once_fn = None
Packit Service a04d08
            content = None
Packit Service a04d08
            if include_once_on:
Packit Service a04d08
                include_once_fn = self._get_include_once_filename(include_url)
Packit Service a04d08
            if include_once_on and os.path.isfile(include_once_fn):
Packit Service a04d08
                content = util.load_file(include_once_fn)
Packit Service a04d08
            else:
Packit Service a04d08
                try:
Packit Service a04d08
                    resp = read_file_or_url(include_url, timeout=5, retries=10,
Packit Service a04d08
                                            ssl_details=self.ssl_details)
Packit Service a04d08
                    if include_once_on and resp.ok():
Packit Service a04d08
                        util.write_file(include_once_fn, resp.contents,
Packit Service a04d08
                                        mode=0o600)
Packit Service a04d08
                    if resp.ok():
Packit Service a04d08
                        content = resp.contents
Packit Service a04d08
                    else:
Packit Service 751c4a
                        error_message = (
Packit Service 751c4a
                            "Fetching from {} resulted in"
Packit Service 751c4a
                            " a invalid http code of {}".format(
Packit Service 751c4a
                                include_url, resp.code))
Packit Service 751c4a
                        _handle_error(error_message)
Packit Service a04d08
                except UrlError as urle:
Packit Service a04d08
                    message = str(urle)
Packit Service a04d08
                    # Older versions of requests.exceptions.HTTPError may not
Packit Service a04d08
                    # include the errant url. Append it for clarity in logs.
Packit Service a04d08
                    if include_url not in message:
Packit Service a04d08
                        message += ' for url: {0}'.format(include_url)
Packit Service 751c4a
                    _handle_error(message, urle)
Packit Service a04d08
                except IOError as ioe:
Packit Service 751c4a
                    error_message = "Fetching from {} resulted in {}".format(
Packit Service 751c4a
                        include_url, ioe)
Packit Service 751c4a
                    _handle_error(error_message, ioe)
Packit Service a04d08
Packit Service a04d08
            if content is not None:
Packit Service a04d08
                new_msg = convert_string(content)
Packit Service a04d08
                self._process_msg(new_msg, append_msg)
Packit Service a04d08
Packit Service a04d08
    def _explode_archive(self, archive, append_msg):
Packit Service a04d08
        entries = util.load_yaml(archive, default=[], allowed=(list, set))
Packit Service a04d08
        for ent in entries:
Packit Service a04d08
            # ent can be one of:
Packit Service a04d08
            #  dict { 'filename' : 'value', 'content' :
Packit Service a04d08
            #       'value', 'type' : 'value' }
Packit Service a04d08
            #    filename and type not be present
Packit Service a04d08
            # or
Packit Service a04d08
            #  scalar(payload)
Packit Service 751c4a
            if isinstance(ent, str):
Packit Service a04d08
                ent = {'content': ent}
Packit Service a04d08
            if not isinstance(ent, (dict)):
Packit Service a04d08
                # TODO(harlowja) raise?
Packit Service a04d08
                continue
Packit Service a04d08
Packit Service a04d08
            content = ent.get('content', '')
Packit Service a04d08
            mtype = ent.get('type')
Packit Service a04d08
            if not mtype:
Packit Service a04d08
                default = ARCHIVE_UNDEF_TYPE
Packit Service 751c4a
                if isinstance(content, bytes):
Packit Service a04d08
                    default = ARCHIVE_UNDEF_BINARY_TYPE
Packit Service a04d08
                mtype = handlers.type_from_starts_with(content, default)
Packit Service a04d08
Packit Service a04d08
            maintype, subtype = mtype.split('/', 1)
Packit Service a04d08
            if maintype == "text":
Packit Service 751c4a
                if isinstance(content, bytes):
Packit Service a04d08
                    content = content.decode()
Packit Service a04d08
                msg = MIMEText(content, _subtype=subtype)
Packit Service a04d08
            else:
Packit Service a04d08
                msg = MIMEBase(maintype, subtype)
Packit Service a04d08
                msg.set_payload(content)
Packit Service a04d08
Packit Service a04d08
            if 'filename' in ent:
Packit Service a04d08
                _set_filename(msg, ent['filename'])
Packit Service a04d08
            if 'launch-index' in ent:
Packit Service a04d08
                msg.add_header('Launch-Index', str(ent['launch-index']))
Packit Service a04d08
Packit Service a04d08
            for header in list(ent.keys()):
Packit Service a04d08
                if header.lower() in ('content', 'filename', 'type',
Packit Service a04d08
                                      'launch-index', 'content-disposition',
Packit Service a04d08
                                      ATTACHMENT_FIELD.lower(),
Packit Service a04d08
                                      CONTENT_TYPE.lower()):
Packit Service a04d08
                    continue
Packit Service a04d08
                msg.add_header(header, ent[header])
Packit Service a04d08
Packit Service a04d08
            self._attach_part(append_msg, msg)
Packit Service a04d08
Packit Service a04d08
    def _multi_part_count(self, outer_msg, new_count=None):
Packit Service a04d08
        """
Packit Service a04d08
        Return the number of attachments to this MIMEMultipart by looking
Packit Service a04d08
        at its 'Number-Attachments' header.
Packit Service a04d08
        """
Packit Service a04d08
        if ATTACHMENT_FIELD not in outer_msg:
Packit Service a04d08
            outer_msg[ATTACHMENT_FIELD] = '0'
Packit Service a04d08
Packit Service a04d08
        if new_count is not None:
Packit Service a04d08
            _replace_header(outer_msg, ATTACHMENT_FIELD, str(new_count))
Packit Service a04d08
Packit Service a04d08
        fetched_count = 0
Packit Service a04d08
        try:
Packit Service a04d08
            fetched_count = int(outer_msg.get(ATTACHMENT_FIELD))
Packit Service a04d08
        except (ValueError, TypeError):
Packit Service a04d08
            _replace_header(outer_msg, ATTACHMENT_FIELD, str(fetched_count))
Packit Service a04d08
        return fetched_count
Packit Service a04d08
Packit Service a04d08
    def _attach_part(self, outer_msg, part):
Packit Service a04d08
        """
Packit Service a04d08
        Attach a message to an outer message. outermsg must be a MIMEMultipart.
Packit Service a04d08
        Modifies a header in the outer message to keep track of number of
Packit Service a04d08
        attachments.
Packit Service a04d08
        """
Packit Service a04d08
        part_count = self._multi_part_count(outer_msg)
Packit Service a04d08
        self._process_before_attach(part, part_count + 1)
Packit Service a04d08
        outer_msg.attach(part)
Packit Service a04d08
        self._multi_part_count(outer_msg, part_count + 1)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def is_skippable(part):
Packit Service a04d08
    # multipart/* are just containers
Packit Service a04d08
    part_maintype = part.get_content_maintype() or ''
Packit Service a04d08
    if part_maintype.lower() == 'multipart':
Packit Service a04d08
        return True
Packit Service a04d08
    return False
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
# Coverts a raw string into a mime message
Packit Service a04d08
def convert_string(raw_data, content_type=NOT_MULTIPART_TYPE):
Packit Service a04d08
    """convert a string (more likely bytes) or a message into
Packit Service a04d08
    a mime message."""
Packit Service a04d08
    if not raw_data:
Packit Service a04d08
        raw_data = b''
Packit Service a04d08
Packit Service a04d08
    def create_binmsg(data, content_type):
Packit Service a04d08
        maintype, subtype = content_type.split("/", 1)
Packit Service a04d08
        msg = MIMEBase(maintype, subtype)
Packit Service a04d08
        msg.set_payload(data)
Packit Service a04d08
        return msg
Packit Service a04d08
Packit Service 751c4a
    if isinstance(raw_data, str):
Packit Service a04d08
        bdata = raw_data.encode('utf-8')
Packit Service a04d08
    else:
Packit Service a04d08
        bdata = raw_data
Packit Service a04d08
    bdata = util.decomp_gzip(bdata, decode=False)
Packit Service a04d08
    if b"mime-version:" in bdata[0:4096].lower():
Packit Service a04d08
        msg = util.message_from_string(bdata.decode('utf-8'))
Packit Service a04d08
    else:
Packit Service a04d08
        msg = create_binmsg(bdata, content_type)
Packit Service a04d08
Packit Service a04d08
    return msg
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
# vi: ts=4 expandtab