DataSourceNoCloud.py 9.98 KB
Newer Older
1 2 3
# vi: ts=4 expandtab
#
#    Copyright (C) 2009-2010 Canonical Ltd.
4
#    Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
5
#    Copyright (C) 2012 Yahoo! Inc.
6 7
#
#    Author: Scott Moser <scott.moser@canonical.com>
8
#    Author: Juerg Hafliger <juerg.haefliger@hp.com>
9
#    Author: Joshua Harlow <harlowja@yahoo-inc.com>
10 11 12 13 14 15 16 17 18 19 20 21 22
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License version 3, as
#    published by the Free Software Foundation.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

23
import errno
24 25 26 27 28 29 30
import os

from cloudinit import log as logging
from cloudinit import sources
from cloudinit import util

LOG = logging.getLogger(__name__)
31

32

33 34 35 36 37 38 39 40
class DataSourceNoCloud(sources.DataSource):
    def __init__(self, sys_cfg, distro, paths):
        sources.DataSource.__init__(self, sys_cfg, distro, paths)
        self.dsmode = 'local'
        self.seed = None
        self.cmdline_id = "ds=nocloud"
        self.seed_dir = os.path.join(paths.seed_dir, 'nocloud')
        self.supported_seed_starts = ("/", "file://")
41 42

    def __str__(self):
43 44
        root = sources.DataSource.__str__(self)
        return "%s [seed=%s][dsmode=%s]" % (root, self.seed, self.dsmode)
45 46

    def get_data(self):
47
        defaults = {
48 49
            "instance-id": "nocloud",
            "dsmode": self.dsmode,
50 51
        }

52
        found = []
53
        mydata = {'meta-data': {}, 'user-data': "", 'vendor-data': ""}
54 55

        try:
56
            # Parse the kernel command line, getting data passed in
57
            md = {}
58
            if parse_cmdline_data(self.cmdline_id, md):
59
                found.append("cmdline")
60
            mydata['meta-data'].update(md)
61
        except:
62
            util.logexc(LOG, "Unable to parse command line data")
63 64
            return False

65
        # Check to see if the seed dir has data.
66 67 68 69 70
        pp2d_kwargs = {'required': ['user-data', 'meta-data'],
                       'optional': ['vendor-data']}

        try:
            seeded = util.pathprefix2dict(self.seed_dir, **pp2d_kwargs)
71
            found.append(self.seed_dir)
72 73 74 75 76 77
            LOG.debug("Using seeded data from %s", self.seed_dir)
        except ValueError as e:
            pass

        if self.seed_dir in found:
            mydata = _merge_new_seed(mydata, seeded)
78

79
        # If the datasource config had a 'seedfrom' entry, then that takes
80
        # precedence over a 'seedfrom' that was found in a filesystem
81
        # but not over external media
82 83 84
        if self.ds_cfg.get('seedfrom'):
            found.append("ds_config_seedfrom")
            mydata['meta-data']["seedfrom"] = self.ds_cfg['seedfrom']
85

86 87
        # fields appropriately named can also just come from the datasource
        # config (ie, 'user-data', 'meta-data', 'vendor-data' there)
88
        if 'user-data' in self.ds_cfg and 'meta-data' in self.ds_cfg:
89 90 91 92
            mydata = _merge_new_seed(mydata, self.ds_cfg)
            found.append("ds_config")

        def _pp2d_callback(mp, data):
93
            return util.pathprefix2dict(mp, **data)
94

95 96
        label = self.ds_cfg.get('fs_label', "cidata")
        if label is not None:
97 98
            # Query optical drive to get it in blkid cache for 2.6 kernels
            util.find_devs_with(path="/dev/sr0")
99
            util.find_devs_with(path="/dev/sr1")
100

101 102 103 104 105 106 107 108 109 110 111
            fslist = util.find_devs_with("TYPE=vfat")
            fslist.extend(util.find_devs_with("TYPE=iso9660"))

            label_list = util.find_devs_with("LABEL=%s" % label)
            devlist = list(set(fslist) & set(label_list))
            devlist.sort(reverse=True)

            for dev in devlist:
                try:
                    LOG.debug("Attempting to use data from %s", dev)

112
                    try:
113 114
                        seeded = util.mount_cb(dev, _pp2d_callback,
                                               pp2d_kwargs)
115 116 117 118 119 120 121
                    except ValueError as e:
                        if dev in label_list:
                            LOG.warn("device %s with label=%s not a"
                                     "valid seed.", dev, label)
                        continue

                    mydata = _merge_new_seed(mydata, seeded)
122 123 124 125

                    # For seed from a device, the default mode is 'net'.
                    # that is more likely to be what is desired.  If they want
                    # dsmode of local, then they must specify that.
126
                    if 'dsmode' not in mydata['meta-data']:
127
                        mydata['dsmode'] = "net"
128 129 130 131 132 133 134 135

                    LOG.debug("Using data from %s", dev)
                    found.append(dev)
                    break
                except OSError as e:
                    if e.errno != errno.ENOENT:
                        raise
                except util.MountFailedError:
136 137
                    util.logexc(LOG, "Failed to mount %s when looking for "
                                "data", dev)
138

139
        # There was no indication on kernel cmdline or data
140
        # in the seeddir suggesting this handler should be used.
141
        if len(found) == 0:
142 143
            return False

144 145
        seeded_interfaces = None

146
        # The special argument "seedfrom" indicates we should
147
        # attempt to seed the userdata / metadata from its value
148 149
        # its primarily value is in allowing the user to type less
        # on the command line, ie: ds=nocloud;s=http://bit.ly/abcdefg
150 151
        if "seedfrom" in mydata['meta-data']:
            seedfrom = mydata['meta-data']["seedfrom"]
152
            seedfound = False
153 154
            for proto in self.supported_seed_starts:
                if seedfrom.startswith(proto):
155
                    seedfound = proto
156
                    break
157
            if not seedfound:
158
                LOG.debug("Seed from %s not supported by %s", seedfrom, self)
159 160
                return False

161
            if 'network-interfaces' in mydata['meta-data']:
162 163
                seeded_interfaces = self.dsmode

164
            # This could throw errors, but the user told us to do it
165
            # so if errors are raised, let them raise
166
            (md_seed, ud) = util.read_seeded(seedfrom, timeout=None)
167
            LOG.debug("Using seeded cache data from %s", seedfrom)
168

169
            # Values in the command line override those from the seed
170 171 172
            mydata['meta-data'] = util.mergemanydict([mydata['meta-data'],
                                                      md_seed])
            mydata['user-data'] = ud
173
            found.append(seedfrom)
174

175
        # Now that we have exhausted any other places merge in the defaults
176 177
        mydata['meta-data'] = util.mergemanydict([mydata['meta-data'],
                                                  defaults])
178

179
        # Update the network-interfaces if metadata had 'network-interfaces'
180 181 182
        # entry and this is the local datasource, or 'seedfrom' was used
        # and the source of the seed was self.dsmode
        # ('local' for NoCloud, 'net' for NoCloudNet')
183
        if ('network-interfaces' in mydata['meta-data'] and
184
                (self.dsmode in ("local", seeded_interfaces))):
185
            LOG.debug("Updating network interfaces from %s", self)
186 187
            self.distro.apply_network(
                mydata['meta-data']['network-interfaces'])
188

189
        if mydata['meta-data']['dsmode'] == self.dsmode:
190
            self.seed = ",".join(found)
191 192 193
            self.metadata = mydata['meta-data']
            self.userdata_raw = mydata['user-data']
            self.vendordata = mydata['vendor-data']
194 195
            return True

196
        LOG.debug("%s: not claiming datasource, dsmode=%s", self, md['dsmode'])
197
        return False
198

199

200
# Returns true or false indicating if cmdline indicated
201
# that this module should be used
202
# Example cmdline:
203
#  root=LABEL=uec-rootfs ro ds=nocloud
204
def parse_cmdline_data(ds_id, fill, cmdline=None):
205
    if cmdline is None:
206
        cmdline = util.get_cmdline()
207
    cmdline = " %s " % cmdline
208

209
    if not (" %s " % ds_id in cmdline or " %s;" % ds_id in cmdline):
210
        return False
211

212
    argline = ""
213 214 215
    # cmdline can contain:
    # ds=nocloud[;key=val;key=val]
    for tok in cmdline.split():
216
        if tok.startswith(ds_id):
217
            argline = tok.split("=", 1)
218

219 220
    # argline array is now 'nocloud' followed optionally by
    # a ';' and then key=value pairs also terminated with ';'
221
    tmp = argline[1].split(";")
222
    if len(tmp) > 1:
223
        kvpairs = tmp[1:]
224
    else:
225
        kvpairs = ()
226 227

    # short2long mapping to save cmdline typing
228
    s2l = {"h": "local-hostname", "i": "instance-id", "s": "seedfrom"}
229
    for item in kvpairs:
230 231
        if item == "":
            continue
232
        try:
233
            (k, v) = item.split("=", 1)
234
        except:
235 236
            k = item
            v = None
237
        if k in s2l:
238 239
            k = s2l[k]
        fill[k] = v
240

241
    return True
242

243

244 245 246 247 248 249 250 251 252 253
def _merge_new_seed(cur, seeded):
    ret = cur.copy()
    ret['meta-data'] = util.mergemanydict([cur['meta-data'],
                                          util.load_yaml(seeded['meta-data'])])
    ret['user-data'] = seeded['user-data']
    if 'vendor-data' in seeded:
        ret['vendor-data'] = seeded['vendor-data']
    return ret


254
class DataSourceNoCloudNet(DataSourceNoCloud):
255 256 257 258 259 260
    def __init__(self, sys_cfg, distro, paths):
        DataSourceNoCloud.__init__(self, sys_cfg, distro, paths)
        self.cmdline_id = "ds=nocloud-net"
        self.supported_seed_starts = ("http://", "https://", "ftp://")
        self.seed_dir = os.path.join(paths.seed_dir, 'nocloud-net')
        self.dsmode = "net"
261

262

263 264 265 266 267
# Used to match classes to dependencies
datasources = [
  (DataSourceNoCloud, (sources.DEP_FILESYSTEM, )),
  (DataSourceNoCloudNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
]
268

269

270
# Return a list of data sources that match this set of dependencies
271
def get_datasource_list(depends):
272
    return sources.list_from_depends(depends, datasources)