~cloud-init-dev/cloud-init/trunk

« back to all changes in this revision

Viewing changes to cloudinit/handlers/cloud_config.py

  • Committer: Scott Moser
  • Date: 2016-08-10 15:06:15 UTC
  • Revision ID: smoser@ubuntu.com-20160810150615-ma2fv107w3suy1ma
README: Mention move of revision control to git.

cloud-init development has moved its revision control to git.
It is available at 
  https://code.launchpad.net/cloud-init

Clone with 
  git clone https://git.launchpad.net/cloud-init
or
  git clone git+ssh://git.launchpad.net/cloud-init

For more information see
  https://git.launchpad.net/cloud-init/tree/HACKING.rst

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vi: ts=4 expandtab
2
 
#
3
 
#    Copyright (C) 2012 Canonical Ltd.
4
 
#    Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
5
 
#    Copyright (C) 2012 Yahoo! Inc.
6
 
#
7
 
#    Author: Scott Moser <scott.moser@canonical.com>
8
 
#    Author: Juerg Haefliger <juerg.haefliger@hp.com>
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 jsonpatch
24
 
 
25
 
from cloudinit import handlers
26
 
from cloudinit import log as logging
27
 
from cloudinit import mergers
28
 
from cloudinit import util
29
 
 
30
 
from cloudinit.settings import (PER_ALWAYS)
31
 
 
32
 
LOG = logging.getLogger(__name__)
33
 
 
34
 
MERGE_HEADER = 'Merge-Type'
35
 
 
36
 
# Due to the way the loading of yaml configuration was done previously,
37
 
# where previously each cloud config part was appended to a larger yaml
38
 
# file and then finally that file was loaded as one big yaml file we need
39
 
# to mimic that behavior by altering the default strategy to be replacing
40
 
# keys of prior merges.
41
 
#
42
 
#
43
 
# For example
44
 
# #file 1
45
 
# a: 3
46
 
# #file 2
47
 
# a: 22
48
 
# #combined file (comments not included)
49
 
# a: 3
50
 
# a: 22
51
 
#
52
 
# This gets loaded into yaml with final result {'a': 22}
53
 
DEF_MERGERS = mergers.string_extract_mergers('dict(replace)+list()+str()')
54
 
CLOUD_PREFIX = "#cloud-config"
55
 
JSONP_PREFIX = "#cloud-config-jsonp"
56
 
 
57
 
# The file header -> content types this module will handle.
58
 
CC_TYPES = {
59
 
    JSONP_PREFIX: handlers.type_from_starts_with(JSONP_PREFIX),
60
 
    CLOUD_PREFIX: handlers.type_from_starts_with(CLOUD_PREFIX),
61
 
}
62
 
 
63
 
 
64
 
class CloudConfigPartHandler(handlers.Handler):
65
 
    def __init__(self, paths, **_kwargs):
66
 
        handlers.Handler.__init__(self, PER_ALWAYS, version=3)
67
 
        self.cloud_buf = None
68
 
        self.cloud_fn = paths.get_ipath("cloud_config")
69
 
        if 'cloud_config_path' in _kwargs:
70
 
            self.cloud_fn = paths.get_ipath(_kwargs["cloud_config_path"])
71
 
        self.file_names = []
72
 
 
73
 
    def list_types(self):
74
 
        return list(CC_TYPES.values())
75
 
 
76
 
    def _write_cloud_config(self):
77
 
        if not self.cloud_fn:
78
 
            return
79
 
        # Capture which files we merged from...
80
 
        file_lines = []
81
 
        if self.file_names:
82
 
            file_lines.append("# from %s files" % (len(self.file_names)))
83
 
            for fn in self.file_names:
84
 
                if not fn:
85
 
                    fn = '?'
86
 
                file_lines.append("# %s" % (fn))
87
 
            file_lines.append("")
88
 
        if self.cloud_buf is not None:
89
 
            # Something was actually gathered....
90
 
            lines = [
91
 
                CLOUD_PREFIX,
92
 
                '',
93
 
            ]
94
 
            lines.extend(file_lines)
95
 
            lines.append(util.yaml_dumps(self.cloud_buf))
96
 
        else:
97
 
            lines = []
98
 
        util.write_file(self.cloud_fn, "\n".join(lines), 0o600)
99
 
 
100
 
    def _extract_mergers(self, payload, headers):
101
 
        merge_header_headers = ''
102
 
        for h in [MERGE_HEADER, 'X-%s' % (MERGE_HEADER)]:
103
 
            tmp_h = headers.get(h, '')
104
 
            if tmp_h:
105
 
                merge_header_headers = tmp_h
106
 
                break
107
 
        # Select either the merge-type from the content
108
 
        # or the merge type from the headers or default to our own set
109
 
        # if neither exists (or is empty) from the later.
110
 
        payload_yaml = util.load_yaml(payload)
111
 
        mergers_yaml = mergers.dict_extract_mergers(payload_yaml)
112
 
        mergers_header = mergers.string_extract_mergers(merge_header_headers)
113
 
        all_mergers = []
114
 
        all_mergers.extend(mergers_yaml)
115
 
        all_mergers.extend(mergers_header)
116
 
        if not all_mergers:
117
 
            all_mergers = DEF_MERGERS
118
 
        return (payload_yaml, all_mergers)
119
 
 
120
 
    def _merge_patch(self, payload):
121
 
        # JSON doesn't handle comments in this manner, so ensure that
122
 
        # if we started with this 'type' that we remove it before
123
 
        # attempting to load it as json (which the jsonpatch library will
124
 
        # attempt to do).
125
 
        payload = payload.lstrip()
126
 
        payload = util.strip_prefix_suffix(payload, prefix=JSONP_PREFIX)
127
 
        patch = jsonpatch.JsonPatch.from_string(payload)
128
 
        LOG.debug("Merging by applying json patch %s", patch)
129
 
        self.cloud_buf = patch.apply(self.cloud_buf, in_place=False)
130
 
 
131
 
    def _merge_part(self, payload, headers):
132
 
        (payload_yaml, my_mergers) = self._extract_mergers(payload, headers)
133
 
        LOG.debug("Merging by applying %s", my_mergers)
134
 
        merger = mergers.construct(my_mergers)
135
 
        self.cloud_buf = merger.merge(self.cloud_buf, payload_yaml)
136
 
 
137
 
    def _reset(self):
138
 
        self.file_names = []
139
 
        self.cloud_buf = None
140
 
 
141
 
    def handle_part(self, data, ctype, filename, payload, frequency, headers):
142
 
        if ctype == handlers.CONTENT_START:
143
 
            self._reset()
144
 
            return
145
 
        if ctype == handlers.CONTENT_END:
146
 
            self._write_cloud_config()
147
 
            self._reset()
148
 
            return
149
 
        try:
150
 
            # First time through, merge with an empty dict...
151
 
            if self.cloud_buf is None or not self.file_names:
152
 
                self.cloud_buf = {}
153
 
            if ctype == CC_TYPES[JSONP_PREFIX]:
154
 
                self._merge_patch(payload)
155
 
            else:
156
 
                self._merge_part(payload, headers)
157
 
            # Ensure filename is ok to store
158
 
            for i in ("\n", "\r", "\t"):
159
 
                filename = filename.replace(i, " ")
160
 
            self.file_names.append(filename.strip())
161
 
        except Exception:
162
 
            util.logexc(LOG, "Failed at merging in cloud config part from %s",
163
 
                        filename)