2. Adjust comment on sources list from depends 3. For the /etc/timezone 'writing', add a header that says created by cloud-init
		
			
				
	
	
		
			227 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			227 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# vi: ts=4 expandtab
 | 
						|
#
 | 
						|
#    Copyright (C) 2012 Canonical Ltd.
 | 
						|
#    Copyright (C) 2012 Yahoo! Inc.
 | 
						|
#
 | 
						|
#    Author: Scott Moser <scott.moser@canonical.com>
 | 
						|
#    Author: Joshua Harlow <harlowja@yahoo-inc.com>
 | 
						|
#
 | 
						|
#    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/>.
 | 
						|
 | 
						|
import json
 | 
						|
import os
 | 
						|
 | 
						|
from cloudinit import log as logging
 | 
						|
from cloudinit import sources
 | 
						|
from cloudinit import util
 | 
						|
 | 
						|
LOG = logging.getLogger(__name__)
 | 
						|
 | 
						|
# Various defaults/constants...
 | 
						|
DEFAULT_IID = "iid-dsconfigdrive"
 | 
						|
DEFAULT_MODE = 'pass'
 | 
						|
CFG_DRIVE_FILES = [
 | 
						|
    "etc/network/interfaces",
 | 
						|
    "root/.ssh/authorized_keys",
 | 
						|
    "meta.js",
 | 
						|
]
 | 
						|
DEFAULT_METADATA = {
 | 
						|
    "instance-id": DEFAULT_IID,
 | 
						|
    "dsmode": DEFAULT_MODE,
 | 
						|
}
 | 
						|
CFG_DRIVE_DEV_ENV = 'CLOUD_INIT_CONFIG_DRIVE_DEVICE'
 | 
						|
 | 
						|
 | 
						|
class DataSourceConfigDrive(sources.DataSource):
 | 
						|
    def __init__(self, sys_cfg, distro, paths):
 | 
						|
        sources.DataSource.__init__(self, sys_cfg, distro, paths)
 | 
						|
        self.seed = None
 | 
						|
        self.cfg = {}
 | 
						|
        self.dsmode = 'local'
 | 
						|
        self.seed_dir = os.path.join(paths.seed_dir, 'config_drive')
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        mstr = "%s [%s]" % (util.obj_name(self), self.dsmode)
 | 
						|
        mstr += "[seed=%s]" % (self.seed)
 | 
						|
        return mstr
 | 
						|
 | 
						|
    def get_data(self):
 | 
						|
        found = None
 | 
						|
        md = {}
 | 
						|
        ud = ""
 | 
						|
 | 
						|
        if os.path.isdir(self.seed_dir):
 | 
						|
            try:
 | 
						|
                (md, ud) = read_config_drive_dir(self.seed_dir)
 | 
						|
                found = self.seed_dir
 | 
						|
            except NonConfigDriveDir:
 | 
						|
                util.logexc(LOG, "Failed reading config drive from %s",
 | 
						|
                            self.seed_dir)
 | 
						|
        if not found:
 | 
						|
            dev = find_cfg_drive_device()
 | 
						|
            if dev:
 | 
						|
                try:
 | 
						|
                    (md, ud) = util.mount_cb(dev, read_config_drive_dir)
 | 
						|
                    found = dev
 | 
						|
                except (NonConfigDriveDir, util.MountFailedError):
 | 
						|
                    pass
 | 
						|
 | 
						|
        if not found:
 | 
						|
            return False
 | 
						|
 | 
						|
        if 'dsconfig' in md:
 | 
						|
            self.cfg = md['dscfg']
 | 
						|
 | 
						|
        md = util.mergedict(md, DEFAULT_METADATA)
 | 
						|
 | 
						|
        # Update interfaces and ifup only on the local datasource
 | 
						|
        # this way the DataSourceConfigDriveNet doesn't do it also.
 | 
						|
        if 'network-interfaces' in md and self.dsmode == "local":
 | 
						|
            LOG.debug("Updating network interfaces from config drive (%s)",
 | 
						|
                     md['dsmode'])
 | 
						|
            self.distro.apply_network(md['network-interfaces'])
 | 
						|
 | 
						|
        self.seed = found
 | 
						|
        self.metadata = md
 | 
						|
        self.userdata_raw = ud
 | 
						|
 | 
						|
        if md['dsmode'] == self.dsmode:
 | 
						|
            return True
 | 
						|
 | 
						|
        LOG.debug("%s: not claiming datasource, dsmode=%s", self, md['dsmode'])
 | 
						|
        return False
 | 
						|
 | 
						|
    def get_public_ssh_keys(self):
 | 
						|
        if not 'public-keys' in self.metadata:
 | 
						|
            return []
 | 
						|
        return self.metadata['public-keys']
 | 
						|
 | 
						|
    # The data sources' config_obj is a cloud-config formated
 | 
						|
    # object that came to it from ways other than cloud-config
 | 
						|
    # because cloud-config content would be handled elsewhere
 | 
						|
    def get_config_obj(self):
 | 
						|
        return self.cfg
 | 
						|
 | 
						|
 | 
						|
class DataSourceConfigDriveNet(DataSourceConfigDrive):
 | 
						|
    def __init__(self, sys_cfg, distro, paths):
 | 
						|
        DataSourceConfigDrive.__init__(self, sys_cfg, distro, paths)
 | 
						|
        self.dsmode = 'net'
 | 
						|
 | 
						|
 | 
						|
class NonConfigDriveDir(Exception):
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
def find_cfg_drive_device():
 | 
						|
    """ Get the config drive device.  Return a string like '/dev/vdb'
 | 
						|
        or None (if there is no non-root device attached). This does not
 | 
						|
        check the contents, only reports that if there *were* a config_drive
 | 
						|
        attached, it would be this device.
 | 
						|
        Note: per config_drive documentation, this is
 | 
						|
        "associated as the last available disk on the instance"
 | 
						|
    """
 | 
						|
 | 
						|
    # This seems to be for debugging??
 | 
						|
    if CFG_DRIVE_DEV_ENV in os.environ:
 | 
						|
        return os.environ[CFG_DRIVE_DEV_ENV]
 | 
						|
 | 
						|
    # We are looking for a raw block device (sda, not sda1) with a vfat
 | 
						|
    # filesystem on it....
 | 
						|
    letters = "abcdefghijklmnopqrstuvwxyz"
 | 
						|
    devs = util.find_devs_with("TYPE=vfat")
 | 
						|
 | 
						|
    # Filter out anything not ending in a letter (ignore partitions)
 | 
						|
    devs = [f for f in devs if f[-1] in letters]
 | 
						|
 | 
						|
    # Sort them in reverse so "last" device is first
 | 
						|
    devs.sort(reverse=True)
 | 
						|
 | 
						|
    if devs:
 | 
						|
        return devs[0]
 | 
						|
 | 
						|
    return None
 | 
						|
 | 
						|
 | 
						|
def read_config_drive_dir(source_dir):
 | 
						|
    """
 | 
						|
    read_config_drive_dir(source_dir):
 | 
						|
       read source_dir, and return a tuple with metadata dict and user-data
 | 
						|
       string populated.  If not a valid dir, raise a NonConfigDriveDir
 | 
						|
    """
 | 
						|
 | 
						|
    # TODO: fix this for other operating systems...
 | 
						|
    # Ie: this is where https://fedorahosted.org/netcf/ or similar should
 | 
						|
    # be hooked in... (or could be)
 | 
						|
    found = {}
 | 
						|
    for af in CFG_DRIVE_FILES:
 | 
						|
        fn = os.path.join(source_dir, af)
 | 
						|
        if os.path.isfile(fn):
 | 
						|
            found[af] = fn
 | 
						|
 | 
						|
    if len(found) == 0:
 | 
						|
        raise NonConfigDriveDir("%s: %s" % (source_dir, "no files found"))
 | 
						|
 | 
						|
    md = {}
 | 
						|
    ud = ""
 | 
						|
    keydata = ""
 | 
						|
    if "etc/network/interfaces" in found:
 | 
						|
        fn = found["etc/network/interfaces"]
 | 
						|
        md['network-interfaces'] = util.load_file(fn)
 | 
						|
 | 
						|
    if "root/.ssh/authorized_keys" in found:
 | 
						|
        fn = found["root/.ssh/authorized_keys"]
 | 
						|
        keydata = util.load_file(fn)
 | 
						|
 | 
						|
    meta_js = {}
 | 
						|
    if "meta.js" in found:
 | 
						|
        fn = found['meta.js']
 | 
						|
        content = util.load_file(fn)
 | 
						|
        try:
 | 
						|
            # Just check if its really json...
 | 
						|
            meta_js = json.loads(content)
 | 
						|
            if not isinstance(meta_js, (dict)):
 | 
						|
                raise TypeError("Dict expected for meta.js root node")
 | 
						|
        except (ValueError, TypeError) as e:
 | 
						|
            raise NonConfigDriveDir("%s: %s, %s" %
 | 
						|
                (source_dir, "invalid json in meta.js", e))
 | 
						|
        md['meta_js'] = content
 | 
						|
 | 
						|
    # Key data override??
 | 
						|
    keydata = meta_js.get('public-keys', keydata)
 | 
						|
    if keydata:
 | 
						|
        lines = keydata.splitlines()
 | 
						|
        md['public-keys'] = [l for l in lines
 | 
						|
            if len(l) and not l.startswith("#")]
 | 
						|
 | 
						|
    for copy in ('dsmode', 'instance-id', 'dscfg'):
 | 
						|
        if copy in meta_js:
 | 
						|
            md[copy] = meta_js[copy]
 | 
						|
 | 
						|
    if 'user-data' in meta_js:
 | 
						|
        ud = meta_js['user-data']
 | 
						|
 | 
						|
    return (md, ud)
 | 
						|
 | 
						|
 | 
						|
# Used to match classes to dependencies
 | 
						|
datasources = [
 | 
						|
  (DataSourceConfigDrive, (sources.DEP_FILESYSTEM, )),
 | 
						|
  (DataSourceConfigDriveNet, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
 | 
						|
]
 | 
						|
 | 
						|
 | 
						|
# Return a list of data sources that match this set of dependencies
 | 
						|
def get_datasource_list(depends):
 | 
						|
    return sources.list_from_depends(depends, datasources)
 |