merge in work form ds-rework

rework of DataSource loading.

The DataSources that are loaded are now controlled entirely via
configuration file of 'datasource_list', like:
   datasource_list: [ "NoCloud", "OVF", "Ec2" ]

Each item in that list is a "DataSourceCollection".  for each item
in the list, cloudinit will attempt to load:
  cloudinit.DataSource<item>
and, failing that,
  DataSource<item>

The module is required to have a method named 'get_datasource_list'
in it that takes a single list of "dependencies" and returns
a list of python classes inside the collection that can run needing
only those dependencies.

The dependencies are defines in DataSource.py. Currently:
  DEP_FILESYSTEM = "FILESYSTEM"
  DEP_NETWORK = "NETWORK"

When 'get_datasource_list' is called for the DataSourceOVF module with
[DEP_FILESYSTEM], then DataSourceOVF returns a single item list with a
reference to the 'DataSourceOVF' class.

When 'get_datasource_list' is called for the DataSourceOVF module with
[DEP_FILESYSTEM, DEP_NETWORK], it will return a single item list
with a reference to 'DataSourceOVFNet'.

cloudinit will then instanciate the class and call its 'get_data' method.
if the get_data method returns 'True', then it selects this class as the
selected Datasource.
This commit is contained in:
Scott Moser 2011-01-26 13:54:18 -05:00
commit 319a824e04
8 changed files with 159 additions and 78 deletions

View File

@ -31,3 +31,7 @@
via the config file, or user data config file via the config file, or user data config file
- add support for posting data about the instance to a url (phone_home) - add support for posting data about the instance to a url (phone_home)
- add minimal OVF transport (iso) support - add minimal OVF transport (iso) support
- make DataSources that are attempted dynamic and configurable from
system config. changen "cloud_type: auto" as configuration for this
to 'datasource_list: [ "Ec2" ]'. Each of the items in that list
must be modules that can be loaded by "DataSource<item>"

View File

@ -23,6 +23,7 @@ import sys
import cloudinit import cloudinit
import cloudinit.util as util import cloudinit.util as util
import cloudinit.CloudConfig as CC import cloudinit.CloudConfig as CC
import cloudinit.DataSource as ds
import time import time
import logging import logging
import errno import errno
@ -32,10 +33,19 @@ def warn(wstr):
def main(): def main():
cmds = ( "start", "start-local" ) cmds = ( "start", "start-local" )
deps = { "start" : ( ds.DEP_FILESYSTEM, ds.DEP_NETWORK ),
"start-local" : ( ds.DEP_FILESYSTEM, ) }
cmd = "" cmd = ""
if len(sys.argv) > 1: if len(sys.argv) > 1:
cmd = sys.argv[1] cmd = sys.argv[1]
cfg_path = None
if len(sys.argv) > 2:
# this is really for debugging only
# but you can invoke on development system with ./config/cloud.cfg
cfg_path = sys.argv[2]
if not cmd in cmds: if not cmd in cmds:
sys.stderr.write("bad command %s. use one of %s\n" % (cmd, cmds)) sys.stderr.write("bad command %s. use one of %s\n" % (cmd, cmds))
sys.exit(1) sys.exit(1)
@ -49,12 +59,17 @@ def main():
warn("unable to open /proc/uptime\n") warn("unable to open /proc/uptime\n")
uptime = "na" uptime = "na"
source_type = "all" try:
if cmd == "start-local": cfg = cloudinit.get_base_cfg(cfg_path)
source_type = "local" except Exception as e:
warn("Failed to get base config. falling back to builtin: %s\n" % e)
try:
cfg = cloudinit.get_builtin_cfg()
except Exception as e:
warn("Unable to load builtin config\n")
raise
try: try:
cfg = cloudinit.get_base_cfg()
(outfmt, errfmt) = CC.get_output_cfg(cfg,"init") (outfmt, errfmt) = CC.get_output_cfg(cfg,"init")
CC.redirect_output(outfmt, errfmt) CC.redirect_output(outfmt, errfmt)
except Exception as e: except Exception as e:
@ -80,7 +95,7 @@ def main():
if cmd == "start-local": if cmd == "start-local":
cloudinit.purge_cache() cloudinit.purge_cache()
cloud = cloudinit.CloudInit(source_type=source_type) cloud = cloudinit.CloudInit(ds_deps=deps[cmd])
try: try:
cloud.get_data_source() cloud.get_data_source()

View File

@ -16,16 +16,23 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import cloudinit
DEP_FILESYSTEM = "FILESYSTEM"
DEP_NETWORK = "NETWORK"
import UserDataHandler as ud import UserDataHandler as ud
class DataSource: class DataSource:
userdata = None userdata = None
metadata = None metadata = None
userdata_raw = None userdata_raw = None
log = None
def __init__(self): def __init__(self, log=None):
pass if not log:
import logging
log = logging.log
self.log = log
def get_userdata(self): def get_userdata(self):
if self.userdata == None: if self.userdata == None:
@ -91,3 +98,46 @@ class DataSource:
return("ip-%s" % '-'.join(r)) return("ip-%s" % '-'.join(r))
except: pass except: pass
return toks[0] return toks[0]
# return a list of classes that have the same depends as 'depends'
# iterate through cfg_list, loading "DataSourceCollections" modules
# and calling their "get_datasource_list".
# return an ordered list of classes that match
#
# - modules must be named "DataSource<item>", where 'item' is an entry
# in cfg_list
# - if pkglist is given, it will iterate try loading from that package
# ie, pkglist=[ "foo", "" ]
# will first try to load foo.DataSource<item>
# then DataSource<item>
def list_sources(cfg_list, depends, pkglist=[]):
retlist = []
for ds_coll in cfg_list:
for pkg in pkglist:
if pkg: pkg="%s." % pkg
try:
mod = __import__("%sDataSource%s" % (pkg, ds_coll))
if pkg:
mod = getattr(mod, "DataSource%s" % ds_coll)
lister = getattr(mod, "get_datasource_list")
retlist.extend(lister(depends))
break
except:
raise
return(retlist)
# depends is a list of dependencies (DEP_FILESYSTEM)
# dslist is a list of 2 item lists
# dslist = [
# ( class, ( depends-that-this-class-needs ) )
# }
# it returns a list of 'class' that matched these deps exactly
# it is a helper function for DataSourceCollections
def list_from_depends(depends, dslist):
retlist = [ ]
depset = set(depends)
for elem in dslist:
(cls, deps) = elem
if depset == set(deps):
retlist.append(cls)
return(retlist)

View File

@ -18,7 +18,7 @@
import DataSource import DataSource
import cloudinit from cloudinit import seeddir
import cloudinit.util as util import cloudinit.util as util
import socket import socket
import urllib2 import urllib2
@ -30,10 +30,7 @@ import errno
class DataSourceEc2(DataSource.DataSource): class DataSourceEc2(DataSource.DataSource):
api_ver = '2009-04-04' api_ver = '2009-04-04'
seeddir = cloudinit.seeddir + '/ec2' seeddir = seeddir + '/ec2'
def __init__(self):
pass
def __str__(self): def __str__(self):
return("DataSourceEc2") return("DataSourceEc2")
@ -43,7 +40,7 @@ class DataSourceEc2(DataSource.DataSource):
if util.read_optional_seed(seedret,base=self.seeddir+ "/"): if util.read_optional_seed(seedret,base=self.seeddir+ "/"):
self.userdata_raw = seedret['user-data'] self.userdata_raw = seedret['user-data']
self.metadata = seedret['meta-data'] self.metadata = seedret['meta-data']
cloudinit.log.debug("using seeded ec2 data in %s" % self.seeddir) self.log.debug("using seeded ec2 data in %s" % self.seeddir)
return True return True
try: try:
@ -105,13 +102,13 @@ class DataSourceEc2(DataSource.DataSource):
reason = "url error [%s]" % e.reason reason = "url error [%s]" % e.reason
if x == 0: if x == 0:
cloudinit.log.warning("waiting for metadata service at %s\n" % url) self.log.warning("waiting for metadata service at %s\n" % url)
cloudinit.log.warning(" %s [%02s/%s]: %s\n" % self.log.warning(" %s [%02s/%s]: %s\n" %
(time.strftime("%H:%M:%S",time.gmtime()), x+1, sleeps, reason)) (time.strftime("%H:%M:%S",time.gmtime()), x+1, sleeps, reason))
time.sleep(sleeptime) time.sleep(sleeptime)
cloudinit.log.critical("giving up on md after %i seconds\n" % self.log.critical("giving up on md after %i seconds\n" %
int(time.time()-starttime)) int(time.time()-starttime))
return False return False
@ -131,7 +128,7 @@ class DataSourceEc2(DataSource.DataSource):
if entname == "ephemeral" and name == "ephemeral0": if entname == "ephemeral" and name == "ephemeral0":
found = device found = device
if found == None: if found == None:
cloudinit.log.warn("unable to convert %s to a device" % name) self.log.warn("unable to convert %s to a device" % name)
return None return None
# LP: #611137 # LP: #611137
@ -154,7 +151,7 @@ class DataSourceEc2(DataSource.DataSource):
for nto in tlist: for nto in tlist:
cand = "/dev/%s%s" % (nto, short[len(nfrom):]) cand = "/dev/%s%s" % (nto, short[len(nfrom):])
if os.path.exists(cand): if os.path.exists(cand):
cloudinit.log.debug("remapped device name %s => %s" % (found,cand)) self.log.debug("remapped device name %s => %s" % (found,cand))
return(cand) return(cand)
return ofound return ofound
@ -165,3 +162,11 @@ class DataSourceEc2(DataSource.DataSource):
(p4 not in self.metadata or self.metadata[p4] == "")): (p4 not in self.metadata or self.metadata[p4] == "")):
return True return True
return False return False
datasources = [
( DataSourceEc2, ( DataSource.DEP_FILESYSTEM , DataSource.DEP_NETWORK ) ),
]
# return a list of data sources that match this set of dependencies
def get_datasource_list(depends):
return(DataSource.list_from_depends(depends, datasources))

View File

@ -18,7 +18,7 @@
import DataSource import DataSource
import cloudinit from cloudinit import seeddir
import cloudinit.util as util import cloudinit.util as util
import sys import sys
import os.path import os.path
@ -32,10 +32,7 @@ class DataSourceNoCloud(DataSource.DataSource):
supported_seed_starts = ( "/" , "file://" ) supported_seed_starts = ( "/" , "file://" )
seed = None seed = None
cmdline_id = "ds=nocloud" cmdline_id = "ds=nocloud"
seeddir = cloudinit.seeddir + '/nocloud' seeddir = seeddir + '/nocloud'
def __init__(self):
pass
def __str__(self): def __str__(self):
mstr="DataSourceNoCloud" mstr="DataSourceNoCloud"
@ -57,7 +54,7 @@ class DataSourceNoCloud(DataSource.DataSource):
if parse_cmdline_data(self.cmdline_id, md): if parse_cmdline_data(self.cmdline_id, md):
found.append("cmdline") found.append("cmdline")
except: except:
util.logexc(cloudinit.log,util.WARN) util.logexc(self.log,util.WARN)
return False return False
# check to see if the seeddir has data. # check to see if the seeddir has data.
@ -66,7 +63,7 @@ class DataSourceNoCloud(DataSource.DataSource):
md = util.mergedict(md,seedret['meta-data']) md = util.mergedict(md,seedret['meta-data'])
ud = seedret['user-data'] ud = seedret['user-data']
found.append(self.seeddir) found.append(self.seeddir)
cloudinit.log.debug("using seeded cache data in %s" % self.seeddir) self.log.debug("using seeded cache data in %s" % self.seeddir)
# there was no indication on kernel cmdline or data # there was no indication on kernel cmdline or data
# in the seeddir suggesting this handler should be used. # in the seeddir suggesting this handler should be used.
@ -83,14 +80,14 @@ class DataSourceNoCloud(DataSource.DataSource):
seedfound=proto seedfound=proto
break break
if not seedfound: if not seedfound:
cloudinit.log.debug("seed from %s not supported by %s" % self.log.debug("seed from %s not supported by %s" %
(seedfrom, self.__class__)) (seedfrom, self.__class__))
return False return False
# this could throw errors, but the user told us to do it # this could throw errors, but the user told us to do it
# so if errors are raised, let them raise # so if errors are raised, let them raise
(md_seed,ud) = util.read_seeded(seedfrom) (md_seed,ud) = util.read_seeded(seedfrom)
cloudinit.log.debug("using seeded cache data from %s" % seedfrom) self.log.debug("using seeded cache data from %s" % seedfrom)
# values in the command line override those from the seed # values in the command line override those from the seed
md = util.mergedict(md,md_seed) md = util.mergedict(md,md_seed)
@ -143,4 +140,14 @@ def parse_cmdline_data(ds_id,fill,cmdline=None):
class DataSourceNoCloudNet(DataSourceNoCloud): class DataSourceNoCloudNet(DataSourceNoCloud):
cmdline_id = "ds=nocloud-net" cmdline_id = "ds=nocloud-net"
supported_seed_starts = ( "http://", "https://", "ftp://" ) supported_seed_starts = ( "http://", "https://", "ftp://" )
seeddir = cloudinit.seeddir + '/nocloud-net' seeddir = seeddir + '/nocloud-net'
datasources = (
( DataSourceNoCloud, ( DataSource.DEP_FILESYSTEM, ) ),
( DataSourceNoCloudNet,
( DataSource.DEP_FILESYSTEM, DataSource.DEP_NETWORK ) ),
)
# return a list of data sources that match this set of dependencies
def get_datasource_list(depends):
return(DataSource.list_from_depends(depends, datasources))

View File

@ -18,7 +18,7 @@
import DataSource import DataSource
import cloudinit from cloudinit import seeddir
import cloudinit.util as util import cloudinit.util as util
import sys import sys
import os.path import os.path
@ -27,19 +27,19 @@ import errno
from xml.dom import minidom from xml.dom import minidom
from xml.dom import Node from xml.dom import Node
import base64 import base64
import re
import tempfile
import subprocess
class DataSourceOVF(DataSource.DataSource): class DataSourceOVF(DataSource.DataSource):
seed = None seed = None
seeddir = cloudinit.seeddir + '/ovf' seeddir = seeddir + '/ovf'
environment = None environment = None
cfg = { } cfg = { }
userdata_raw = None userdata_raw = None
metadata = None metadata = None
supported_seed_starts = ( "/" , "file://" ) supported_seed_starts = ( "/" , "file://" )
def __init__(self):
pass
def __str__(self): def __str__(self):
mstr="DataSourceOVF" mstr="DataSourceOVF"
mstr = mstr + " [seed=%s]" % self.seed mstr = mstr + " [seed=%s]" % self.seed
@ -87,12 +87,12 @@ class DataSourceOVF(DataSource.DataSource):
seedfound = proto seedfound = proto
break break
if not seedfound: if not seedfound:
cloudinit.log.debug("seed from %s not supported by %s" % self.log.debug("seed from %s not supported by %s" %
(seedfrom, self.__class__)) (seedfrom, self.__class__))
return False return False
(md_seed,ud) = util.read_seeded(seedfrom) (md_seed,ud) = util.read_seeded(seedfrom)
cloudinit.log.debug("using seeded cache data from %s" % seedfrom) self.log.debug("using seeded cache data from %s" % seedfrom)
md = util.mergedict(md,md_seed) md = util.mergedict(md,md_seed)
found.append(seedfrom) found.append(seedfrom)
@ -119,7 +119,7 @@ class DataSourceOVF(DataSource.DataSource):
return(self.cfg) return(self.cfg)
class DataSourceOVFNet(DataSourceOVF): class DataSourceOVFNet(DataSourceOVF):
seeddir = cloudinit.seeddir + '/ovf-net' seeddir = seeddir + '/ovf-net'
supported_seed_starts = ( "http://", "https://", "ftp://" ) supported_seed_starts = ( "http://", "https://", "ftp://" )
# this will return a dict with some content # this will return a dict with some content
@ -280,6 +280,16 @@ def getProperties(environString):
return(props) return(props)
datasources = (
( DataSourceOVF, ( DataSource.DEP_FILESYSTEM, ) ),
( DataSourceOVFNet,
( DataSource.DEP_FILESYSTEM, DataSource.DEP_NETWORK ) ),
)
# return a list of data sources that match this set of dependencies
def get_datasource_list(depends):
return(DataSource.list_from_depends(depends, datasources))
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
envStr = open(sys.argv[1]).read() envStr = open(sys.argv[1]).read()

View File

@ -27,7 +27,7 @@ cfg_env_name = "CLOUD_CFG"
cfg_builtin = """ cfg_builtin = """
log_cfgs: [ ] log_cfgs: [ ]
cloud_type: auto datasource_list: [ "NoCloud", "OVF", "Ec2" ]
def_log_file: /var/log/cloud-init.log def_log_file: /var/log/cloud-init.log
syslog_fix_perms: syslog:adm syslog_fix_perms: syslog:adm
""" """
@ -101,29 +101,17 @@ def logging_set_from_cfg(cfg):
raise Exception("no valid logging found\n") raise Exception("no valid logging found\n")
import DataSourceEc2 import DataSource
import DataSourceNoCloud
import DataSourceOVF
import UserDataHandler import UserDataHandler
class CloudInit: class CloudInit:
datasource_map = {
"ec2" : DataSourceEc2.DataSourceEc2,
"nocloud" : DataSourceNoCloud.DataSourceNoCloud,
"nocloud-net" : DataSourceNoCloud.DataSourceNoCloudNet,
"ovf" : DataSourceOVF.DataSourceOVF,
}
datasource = None
auto_orders = {
"all": ( "nocloud-net", "ec2" ),
"local" : ( "nocloud", "ovf" ),
}
cfg = None cfg = None
part_handlers = { } part_handlers = { }
old_conffile = '/etc/ec2-init/ec2-config.cfg' old_conffile = '/etc/ec2-init/ec2-config.cfg'
source_type = "all" ds_deps = [ DataSource.DEP_FILESYSTEM, DataSource.DEP_NETWORK ]
datasource = None
def __init__(self, source_type = "all", sysconfig=system_config): def __init__(self, ds_deps = None, sysconfig=system_config):
self.part_handlers = { self.part_handlers = {
'text/x-shellscript' : self.handle_user_script, 'text/x-shellscript' : self.handle_user_script,
'text/cloud-config' : self.handle_cloud_config, 'text/cloud-config' : self.handle_cloud_config,
@ -131,15 +119,19 @@ class CloudInit:
'text/part-handler' : self.handle_handler, 'text/part-handler' : self.handle_handler,
'text/cloud-boothook' : self.handle_cloud_boothook 'text/cloud-boothook' : self.handle_cloud_boothook
} }
if ds_deps != None:
self.ds_deps = ds_deps
self.sysconfig=sysconfig self.sysconfig=sysconfig
self.cfg=self.read_cfg() self.cfg=self.read_cfg()
self.source_type = source_type
def read_cfg(self): def read_cfg(self):
if self.cfg: if self.cfg:
return(self.cfg) return(self.cfg)
conf = util.get_base_cfg(self.sysconfig,cfg_builtin, parsed_cfgs) try:
conf = util.get_base_cfg(self.sysconfig,cfg_builtin, parsed_cfgs)
except Exception as e:
conf = get_builtin_cfg()
# support reading the old ConfigObj format file and merging # support reading the old ConfigObj format file and merging
# it into the yaml dictionary # it into the yaml dictionary
@ -182,9 +174,6 @@ class CloudInit:
except: except:
return False return False
def get_cloud_type(self):
pass
def get_data_source(self): def get_data_source(self):
if self.datasource is not None: return True if self.datasource is not None: return True
@ -192,21 +181,14 @@ class CloudInit:
log.debug("restored from cache type %s" % self.datasource) log.debug("restored from cache type %s" % self.datasource)
return True return True
dslist=[ ] cfglist=self.cfg['datasource_list']
cfglist=self.cfg['cloud_type'] dslist = list_sources(cfglist, self.ds_deps)
if cfglist == "auto": dsnames = map(lambda f: f.__name__, dslist)
dslist = self.auto_orders[self.source_type] log.debug("searching for data source in %s" % dsnames)
elif cfglist: for cls in dslist:
for ds in cfglist.split(','): ds = cls.__name__
dslist.append(strip(ds).tolower())
log.debug("searching for data source in [%s]" % str(dslist))
for ds in dslist:
if ds not in self.datasource_map:
log.warn("data source %s not found in map" % ds)
continue
try: try:
s = self.datasource_map[ds]() s = cls(log)
if s.get_data(): if s.get_data():
self.datasource = s self.datasource = s
self.datasource_name = ds self.datasource_name = ds
@ -216,8 +198,9 @@ class CloudInit:
log.warn("get_data of %s raised %s" % (ds,e)) log.warn("get_data of %s raised %s" % (ds,e))
util.logexc(log) util.logexc(log)
pass pass
log.debug("did not find data source from %s" % dslist) msg = "Did not find data source. searched classes: %s" % dsnames
raise DataSourceNotFoundException("Could not find data source") log.debug(msg)
raise DataSourceNotFoundException(msg)
def set_cur_instance(self): def set_cur_instance(self):
try: try:
@ -532,8 +515,15 @@ def get_ipath_cur(name=None):
def get_cpath(name=None): def get_cpath(name=None):
return("%s%s" % (varlibdir, pathmap[name])) return("%s%s" % (varlibdir, pathmap[name]))
def get_base_cfg(): def get_base_cfg(cfg_path=None):
return(util.get_base_cfg(system_config,cfg_builtin,parsed_cfgs)) if cfg_path is None: cfg_path = system_config
return(util.get_base_cfg(cfg_path,cfg_builtin,parsed_cfgs))
def get_builtin_cfg():
return(yaml.load(cfg_builtin))
class DataSourceNotFoundException(Exception): class DataSourceNotFoundException(Exception):
pass pass
def list_sources(cfg_list, depends):
return(DataSource.list_sources(cfg_list,depends, ["cloudinit", "" ]))

View File

@ -1,7 +1,7 @@
cloud: auto
user: ubuntu user: ubuntu
disable_root: 1 disable_root: 1
preserve_hostname: False preserve_hostname: False
# datasource_list: [ "NoCloud", "OVF", "Ec2" ]
cloud_init_modules: cloud_init_modules:
- resizefs - resizefs