summaryrefslogtreecommitdiff
path: root/cloudinit
diff options
context:
space:
mode:
Diffstat (limited to 'cloudinit')
-rw-r--r--cloudinit/cmd/__init__.py21
-rw-r--r--cloudinit/cmd/main.py685
-rw-r--r--cloudinit/config/cc_apt_configure.py151
-rw-r--r--cloudinit/config/cc_growpart.py10
-rw-r--r--cloudinit/cs_utils.py3
-rw-r--r--cloudinit/distros/__init__.py2
-rw-r--r--cloudinit/distros/arch.py4
-rw-r--r--cloudinit/distros/debian.py16
-rw-r--r--cloudinit/distros/rhel.py8
-rw-r--r--cloudinit/gpg.py74
-rw-r--r--cloudinit/net/__init__.py693
-rw-r--r--cloudinit/net/cmdline.py203
-rw-r--r--cloudinit/net/eni.py460
-rw-r--r--cloudinit/net/network_state.py365
-rw-r--r--cloudinit/net/renderer.py48
-rw-r--r--cloudinit/net/sysconfig.py400
-rw-r--r--cloudinit/serial.py50
-rw-r--r--cloudinit/sources/DataSourceAltCloud.py3
-rw-r--r--cloudinit/sources/DataSourceAzure.py2
-rw-r--r--cloudinit/sources/DataSourceConfigDrive.py152
-rw-r--r--cloudinit/sources/DataSourceGCE.py2
-rw-r--r--cloudinit/sources/DataSourceNoCloud.py19
-rw-r--r--cloudinit/sources/DataSourceOVF.py10
-rw-r--r--cloudinit/sources/DataSourceOpenNebula.py2
-rw-r--r--cloudinit/sources/DataSourceSmartOS.py4
-rw-r--r--cloudinit/sources/helpers/openstack.py147
-rw-r--r--cloudinit/stages.py5
-rw-r--r--cloudinit/templater.py16
-rw-r--r--cloudinit/util.py28
29 files changed, 2480 insertions, 1103 deletions
diff --git a/cloudinit/cmd/__init__.py b/cloudinit/cmd/__init__.py
new file mode 100644
index 00000000..da124641
--- /dev/null
+++ b/cloudinit/cmd/__init__.py
@@ -0,0 +1,21 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2012 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+# Copyright (C) 2012 Yahoo! Inc.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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/>.
diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
new file mode 100644
index 00000000..63621c1d
--- /dev/null
+++ b/cloudinit/cmd/main.py
@@ -0,0 +1,685 @@
+#!/usr/bin/python
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2012 Canonical Ltd.
+# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+# Copyright (C) 2012 Yahoo! Inc.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Juerg Haefliger <juerg.haefliger@hp.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 argparse
+import json
+import os
+import sys
+import tempfile
+import time
+import traceback
+
+from cloudinit import patcher
+patcher.patch() # noqa
+
+from cloudinit import log as logging
+from cloudinit import netinfo
+from cloudinit import signal_handler
+from cloudinit import sources
+from cloudinit import stages
+from cloudinit import templater
+from cloudinit import util
+from cloudinit import version
+
+from cloudinit import reporting
+from cloudinit.reporting import events
+
+from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE,
+ CLOUD_CONFIG)
+
+
+# Pretty little cheetah formatted welcome message template
+WELCOME_MSG_TPL = ("Cloud-init v. ${version} running '${action}' at "
+ "${timestamp}. Up ${uptime} seconds.")
+
+# Module section template
+MOD_SECTION_TPL = "cloud_%s_modules"
+
+# Things u can query on
+QUERY_DATA_TYPES = [
+ 'data',
+ 'data_raw',
+ 'instance_id',
+]
+
+# Frequency shortname to full name
+# (so users don't have to remember the full name...)
+FREQ_SHORT_NAMES = {
+ 'instance': PER_INSTANCE,
+ 'always': PER_ALWAYS,
+ 'once': PER_ONCE,
+}
+
+LOG = logging.getLogger()
+
+
+# Used for when a logger may not be active
+# and we still want to print exceptions...
+def print_exc(msg=''):
+ if msg:
+ sys.stderr.write("%s\n" % (msg))
+ sys.stderr.write('-' * 60)
+ sys.stderr.write("\n")
+ traceback.print_exc(file=sys.stderr)
+ sys.stderr.write('-' * 60)
+ sys.stderr.write("\n")
+
+
+def welcome(action, msg=None):
+ if not msg:
+ msg = welcome_format(action)
+ util.multi_log("%s\n" % (msg),
+ console=False, stderr=True, log=LOG)
+ return msg
+
+
+def welcome_format(action):
+ tpl_params = {
+ 'version': version.version_string(),
+ 'uptime': util.uptime(),
+ 'timestamp': util.time_rfc2822(),
+ 'action': action,
+ }
+ return templater.render_string(WELCOME_MSG_TPL, tpl_params)
+
+
+def extract_fns(args):
+ # Files are already opened so lets just pass that along
+ # since it would of broke if it couldn't have
+ # read that file already...
+ fn_cfgs = []
+ if args.files:
+ for fh in args.files:
+ # The realpath is more useful in logging
+ # so lets resolve to that...
+ fn_cfgs.append(os.path.realpath(fh.name))
+ return fn_cfgs
+
+
+def run_module_section(mods, action_name, section):
+ full_section_name = MOD_SECTION_TPL % (section)
+ (which_ran, failures) = mods.run_section(full_section_name)
+ total_attempted = len(which_ran) + len(failures)
+ if total_attempted == 0:
+ msg = ("No '%s' modules to run"
+ " under section '%s'") % (action_name, full_section_name)
+ sys.stderr.write("%s\n" % (msg))
+ LOG.debug(msg)
+ return []
+ else:
+ LOG.debug("Ran %s modules with %s failures",
+ len(which_ran), len(failures))
+ return failures
+
+
+def apply_reporting_cfg(cfg):
+ if cfg.get('reporting'):
+ reporting.update_configuration(cfg.get('reporting'))
+
+
+def main_init(name, args):
+ deps = [sources.DEP_FILESYSTEM, sources.DEP_NETWORK]
+ if args.local:
+ deps = [sources.DEP_FILESYSTEM]
+
+ if not args.local:
+ # See doc/kernel-cmdline.txt
+ #
+ # This is used in maas datasource, in "ephemeral" (read-only root)
+ # environment where the instance netboots to iscsi ro root.
+ # and the entity that controls the pxe config has to configure
+ # the maas datasource.
+ #
+ # Could be used elsewhere, only works on network based (not local).
+ root_name = "%s.d" % (CLOUD_CONFIG)
+ target_fn = os.path.join(root_name, "91_kernel_cmdline_url.cfg")
+ util.read_write_cmdline_url(target_fn)
+
+ # Cloud-init 'init' stage is broken up into the following sub-stages
+ # 1. Ensure that the init object fetches its config without errors
+ # 2. Setup logging/output redirections with resultant config (if any)
+ # 3. Initialize the cloud-init filesystem
+ # 4. Check if we can stop early by looking for various files
+ # 5. Fetch the datasource
+ # 6. Connect to the current instance location + update the cache
+ # 7. Consume the userdata (handlers get activated here)
+ # 8. Construct the modules object
+ # 9. Adjust any subsequent logging/output redirections using the modules
+ # objects config as it may be different from init object
+ # 10. Run the modules for the 'init' stage
+ # 11. Done!
+ if not args.local:
+ w_msg = welcome_format(name)
+ else:
+ w_msg = welcome_format("%s-local" % (name))
+ init = stages.Init(ds_deps=deps, reporter=args.reporter)
+ # Stage 1
+ init.read_cfg(extract_fns(args))
+ # Stage 2
+ outfmt = None
+ errfmt = None
+ try:
+ LOG.debug("Closing stdin")
+ util.close_stdin()
+ (outfmt, errfmt) = util.fixup_output(init.cfg, name)
+ except Exception:
+ util.logexc(LOG, "Failed to setup output redirection!")
+ print_exc("Failed to setup output redirection!")
+ if args.debug:
+ # Reset so that all the debug handlers are closed out
+ LOG.debug(("Logging being reset, this logger may no"
+ " longer be active shortly"))
+ logging.resetLogging()
+ logging.setupLogging(init.cfg)
+ apply_reporting_cfg(init.cfg)
+
+ # Any log usage prior to setupLogging above did not have local user log
+ # config applied. We send the welcome message now, as stderr/out have
+ # been redirected and log now configured.
+ welcome(name, msg=w_msg)
+
+ # Stage 3
+ try:
+ init.initialize()
+ except Exception:
+ util.logexc(LOG, "Failed to initialize, likely bad things to come!")
+ # Stage 4
+ path_helper = init.paths
+ mode = sources.DSMODE_LOCAL if args.local else sources.DSMODE_NETWORK
+
+ if mode == sources.DSMODE_NETWORK:
+ existing = "trust"
+ sys.stderr.write("%s\n" % (netinfo.debug_info()))
+ LOG.debug(("Checking to see if files that we need already"
+ " exist from a previous run that would allow us"
+ " to stop early."))
+ # no-net is written by upstart cloud-init-nonet when network failed
+ # to come up
+ stop_files = [
+ os.path.join(path_helper.get_cpath("data"), "no-net"),
+ ]
+ existing_files = []
+ for fn in stop_files:
+ if os.path.isfile(fn):
+ existing_files.append(fn)
+
+ if existing_files:
+ LOG.debug("[%s] Exiting. stop file %s existed",
+ mode, existing_files)
+ return (None, [])
+ else:
+ LOG.debug("Execution continuing, no previous run detected that"
+ " would allow us to stop early.")
+ else:
+ existing = "check"
+ if util.get_cfg_option_bool(init.cfg, 'manual_cache_clean', False):
+ existing = "trust"
+
+ init.purge_cache()
+ # Delete the non-net file as well
+ util.del_file(os.path.join(path_helper.get_cpath("data"), "no-net"))
+
+ # Stage 5
+ try:
+ init.fetch(existing=existing)
+ # if in network mode, and the datasource is local
+ # then work was done at that stage.
+ if mode == sources.DSMODE_NETWORK and init.datasource.dsmode != mode:
+ LOG.debug("[%s] Exiting. datasource %s in local mode",
+ mode, init.datasource)
+ return (None, [])
+ except sources.DataSourceNotFoundException:
+ # In the case of 'cloud-init init' without '--local' it is a bit
+ # more likely that the user would consider it failure if nothing was
+ # found. When using upstart it will also mentions job failure
+ # in console log if exit code is != 0.
+ if mode == sources.DSMODE_LOCAL:
+ LOG.debug("No local datasource found")
+ else:
+ util.logexc(LOG, ("No instance datasource found!"
+ " Likely bad things to come!"))
+ if not args.force:
+ init.apply_network_config(bring_up=not args.local)
+ LOG.debug("[%s] Exiting without datasource in local mode", mode)
+ if mode == sources.DSMODE_LOCAL:
+ return (None, [])
+ else:
+ return (None, ["No instance datasource found."])
+ else:
+ LOG.debug("[%s] barreling on in force mode without datasource",
+ mode)
+
+ # Stage 6
+ iid = init.instancify()
+ LOG.debug("[%s] %s will now be targeting instance id: %s. new=%s",
+ mode, name, iid, init.is_new_instance())
+
+ init.apply_network_config(bring_up=bool(mode != sources.DSMODE_LOCAL))
+
+ if mode == sources.DSMODE_LOCAL:
+ if init.datasource.dsmode != mode:
+ LOG.debug("[%s] Exiting. datasource %s not in local mode.",
+ mode, init.datasource)
+ return (init.datasource, [])
+ else:
+ LOG.debug("[%s] %s is in local mode, will apply init modules now.",
+ mode, init.datasource)
+
+ # update fully realizes user-data (pulling in #include if necessary)
+ init.update()
+ # Stage 7
+ try:
+ # Attempt to consume the data per instance.
+ # This may run user-data handlers and/or perform
+ # url downloads and such as needed.
+ (ran, _results) = init.cloudify().run('consume_data',
+ init.consume_data,
+ args=[PER_INSTANCE],
+ freq=PER_INSTANCE)
+ if not ran:
+ # Just consume anything that is set to run per-always
+ # if nothing ran in the per-instance code
+ #
+ # See: https://bugs.launchpad.net/bugs/819507 for a little
+ # reason behind this...
+ init.consume_data(PER_ALWAYS)
+ except Exception:
+ util.logexc(LOG, "Consuming user data failed!")
+ return (init.datasource, ["Consuming user data failed!"])
+
+ apply_reporting_cfg(init.cfg)
+
+ # Stage 8 - re-read and apply relevant cloud-config to include user-data
+ mods = stages.Modules(init, extract_fns(args), reporter=args.reporter)
+ # Stage 9
+ try:
+ outfmt_orig = outfmt
+ errfmt_orig = errfmt
+ (outfmt, errfmt) = util.get_output_cfg(mods.cfg, name)
+ if outfmt_orig != outfmt or errfmt_orig != errfmt:
+ LOG.warn("Stdout, stderr changing to (%s, %s)", outfmt, errfmt)
+ (outfmt, errfmt) = util.fixup_output(mods.cfg, name)
+ except Exception:
+ util.logexc(LOG, "Failed to re-adjust output redirection!")
+ logging.setupLogging(mods.cfg)
+
+ # Stage 10
+ return (init.datasource, run_module_section(mods, name, name))
+
+
+def main_modules(action_name, args):
+ name = args.mode
+ # Cloud-init 'modules' stages are broken up into the following sub-stages
+ # 1. Ensure that the init object fetches its config without errors
+ # 2. Get the datasource from the init object, if it does
+ # not exist then that means the main_init stage never
+ # worked, and thus this stage can not run.
+ # 3. Construct the modules object
+ # 4. Adjust any subsequent logging/output redirections using
+ # the modules objects configuration
+ # 5. Run the modules for the given stage name
+ # 6. Done!
+ w_msg = welcome_format("%s:%s" % (action_name, name))
+ init = stages.Init(ds_deps=[], reporter=args.reporter)
+ # Stage 1
+ init.read_cfg(extract_fns(args))
+ # Stage 2
+ try:
+ init.fetch(existing="trust")
+ except sources.DataSourceNotFoundException:
+ # There was no datasource found, theres nothing to do
+ msg = ('Can not apply stage %s, no datasource found! Likely bad '
+ 'things to come!' % name)
+ util.logexc(LOG, msg)
+ print_exc(msg)
+ if not args.force:
+ return [(msg)]
+ # Stage 3
+ mods = stages.Modules(init, extract_fns(args), reporter=args.reporter)
+ # Stage 4
+ try:
+ LOG.debug("Closing stdin")
+ util.close_stdin()
+ util.fixup_output(mods.cfg, name)
+ except Exception:
+ util.logexc(LOG, "Failed to setup output redirection!")
+ if args.debug:
+ # Reset so that all the debug handlers are closed out
+ LOG.debug(("Logging being reset, this logger may no"
+ " longer be active shortly"))
+ logging.resetLogging()
+ logging.setupLogging(mods.cfg)
+ apply_reporting_cfg(init.cfg)
+
+ # now that logging is setup and stdout redirected, send welcome
+ welcome(name, msg=w_msg)
+
+ # Stage 5
+ return run_module_section(mods, name, name)
+
+
+def main_query(name, _args):
+ raise NotImplementedError(("Action '%s' is not"
+ " currently implemented") % (name))
+
+
+def main_single(name, args):
+ # Cloud-init single stage is broken up into the following sub-stages
+ # 1. Ensure that the init object fetches its config without errors
+ # 2. Attempt to fetch the datasource (warn if it doesn't work)
+ # 3. Construct the modules object
+ # 4. Adjust any subsequent logging/output redirections using
+ # the modules objects configuration
+ # 5. Run the single module
+ # 6. Done!
+ mod_name = args.name
+ w_msg = welcome_format(name)
+ init = stages.Init(ds_deps=[], reporter=args.reporter)
+ # Stage 1
+ init.read_cfg(extract_fns(args))
+ # Stage 2
+ try:
+ init.fetch(existing="trust")
+ except sources.DataSourceNotFoundException:
+ # There was no datasource found,
+ # that might be bad (or ok) depending on
+ # the module being ran (so continue on)
+ util.logexc(LOG, ("Failed to fetch your datasource,"
+ " likely bad things to come!"))
+ print_exc(("Failed to fetch your datasource,"
+ " likely bad things to come!"))
+ if not args.force:
+ return 1
+ # Stage 3
+ mods = stages.Modules(init, extract_fns(args), reporter=args.reporter)
+ mod_args = args.module_args
+ if mod_args:
+ LOG.debug("Using passed in arguments %s", mod_args)
+ mod_freq = args.frequency
+ if mod_freq:
+ LOG.debug("Using passed in frequency %s", mod_freq)
+ mod_freq = FREQ_SHORT_NAMES.get(mod_freq)
+ # Stage 4
+ try:
+ LOG.debug("Closing stdin")
+ util.close_stdin()
+ util.fixup_output(mods.cfg, None)
+ except Exception:
+ util.logexc(LOG, "Failed to setup output redirection!")
+ if args.debug:
+ # Reset so that all the debug handlers are closed out
+ LOG.debug(("Logging being reset, this logger may no"
+ " longer be active shortly"))
+ logging.resetLogging()
+ logging.setupLogging(mods.cfg)
+ apply_reporting_cfg(init.cfg)
+
+ # now that logging is setup and stdout redirected, send welcome
+ welcome(name, msg=w_msg)
+
+ # Stage 5
+ (which_ran, failures) = mods.run_single(mod_name,
+ mod_args,
+ mod_freq)
+ if failures:
+ LOG.warn("Ran %s but it failed!", mod_name)
+ return 1
+ elif not which_ran:
+ LOG.warn("Did not run %s, does it exist?", mod_name)
+ return 1
+ else:
+ # Guess it worked
+ return 0
+
+
+def atomic_write_file(path, content, mode='w'):
+ tf = None
+ try:
+ tf = tempfile.NamedTemporaryFile(dir=os.path.dirname(path),
+ delete=False, mode=mode)
+ tf.write(content)
+ tf.close()
+ os.rename(tf.name, path)
+ except Exception as e:
+ if tf is not None:
+ os.unlink(tf.name)
+ raise e
+
+
+def atomic_write_json(path, data):
+ return atomic_write_file(path, json.dumps(data, indent=1) + "\n")
+
+
+def status_wrapper(name, args, data_d=None, link_d=None):
+ if data_d is None:
+ data_d = os.path.normpath("/var/lib/cloud/data")
+ if link_d is None:
+ link_d = os.path.normpath("/run/cloud-init")
+
+ status_path = os.path.join(data_d, "status.json")
+ status_link = os.path.join(link_d, "status.json")
+ result_path = os.path.join(data_d, "result.json")
+ result_link = os.path.join(link_d, "result.json")
+
+ util.ensure_dirs((data_d, link_d,))
+
+ (_name, functor) = args.action
+
+ if name == "init":
+ if args.local:
+ mode = "init-local"
+ else:
+ mode = "init"
+ elif name == "modules":
+ mode = "modules-%s" % args.mode
+ else:
+ raise ValueError("unknown name: %s" % name)
+
+ modes = ('init', 'init-local', 'modules-config', 'modules-final')
+
+ status = None
+ if mode == 'init-local':
+ for f in (status_link, result_link, status_path, result_path):
+ util.del_file(f)
+ else:
+ try:
+ status = json.loads(util.load_file(status_path))
+ except Exception:
+ pass
+
+ if status is None:
+ nullstatus = {
+ 'errors': [],
+ 'start': None,
+ 'finished': None,
+ }
+ status = {'v1': {}}
+ for m in modes:
+ status['v1'][m] = nullstatus.copy()
+ status['v1']['datasource'] = None
+
+ v1 = status['v1']
+ v1['stage'] = mode
+ v1[mode]['start'] = time.time()
+
+ atomic_write_json(status_path, status)
+ util.sym_link(os.path.relpath(status_path, link_d), status_link,
+ force=True)
+
+ try:
+ ret = functor(name, args)
+ if mode in ('init', 'init-local'):
+ (datasource, errors) = ret
+ if datasource is not None:
+ v1['datasource'] = str(datasource)
+ else:
+ errors = ret
+
+ v1[mode]['errors'] = [str(e) for e in errors]
+
+ except Exception as e:
+ util.logexc(LOG, "failed stage %s", mode)
+ print_exc("failed run of stage %s" % mode)
+ v1[mode]['errors'] = [str(e)]
+
+ v1[mode]['finished'] = time.time()
+ v1['stage'] = None
+
+ atomic_write_json(status_path, status)
+
+ if mode == "modules-final":
+ # write the 'finished' file
+ errors = []
+ for m in modes:
+ if v1[m]['errors']:
+ errors.extend(v1[m].get('errors', []))
+
+ atomic_write_json(result_path,
+ {'v1': {'datasource': v1['datasource'],
+ 'errors': errors}})
+ util.sym_link(os.path.relpath(result_path, link_d), result_link,
+ force=True)
+
+ return len(v1[mode]['errors'])
+
+
+def main(sysv_args=None):
+ if sysv_args is not None:
+ parser = argparse.ArgumentParser(prog=sysv_args[0])
+ sysv_args = sysv_args[1:]
+ else:
+ parser = argparse.ArgumentParser()
+
+ # Top level args
+ parser.add_argument('--version', '-v', action='version',
+ version='%(prog)s ' + (version.version_string()))
+ parser.add_argument('--file', '-f', action='append',
+ dest='files',
+ help=('additional yaml configuration'
+ ' files to use'),
+ type=argparse.FileType('rb'))
+ parser.add_argument('--debug', '-d', action='store_true',
+ help=('show additional pre-action'
+ ' logging (default: %(default)s)'),
+ default=False)
+ parser.add_argument('--force', action='store_true',
+ help=('force running even if no datasource is'
+ ' found (use at your own risk)'),
+ dest='force',
+ default=False)
+
+ parser.set_defaults(reporter=None)
+ subparsers = parser.add_subparsers()
+
+ # Each action and its sub-options (if any)
+ parser_init = subparsers.add_parser('init',
+ help=('initializes cloud-init and'
+ ' performs initial modules'))
+ parser_init.add_argument("--local", '-l', action='store_true',
+ help="start in local mode (default: %(default)s)",
+ default=False)
+ # This is used so that we can know which action is selected +
+ # the functor to use to run this subcommand
+ parser_init.set_defaults(action=('init', main_init))
+
+ # These settings are used for the 'config' and 'final' stages
+ parser_mod = subparsers.add_parser('modules',
+ help=('activates modules using '
+ 'a given configuration key'))
+ parser_mod.add_argument("--mode", '-m', action='store',
+ help=("module configuration name "
+ "to use (default: %(default)s)"),
+ default='config',
+ choices=('init', 'config', 'final'))
+ parser_mod.set_defaults(action=('modules', main_modules))
+
+ # These settings are used when you want to query information
+ # stored in the cloud-init data objects/directories/files
+ parser_query = subparsers.add_parser('query',
+ help=('query information stored '
+ 'in cloud-init'))
+ parser_query.add_argument("--name", '-n', action="store",
+ help="item name to query on",
+ required=True,
+ choices=QUERY_DATA_TYPES)
+ parser_query.set_defaults(action=('query', main_query))
+
+ # This subcommand allows you to run a single module
+ parser_single = subparsers.add_parser('single',
+ help=('run a single module '))
+ parser_single.set_defaults(action=('single', main_single))
+ parser_single.add_argument("--name", '-n', action="store",
+ help="module name to run",
+ required=True)
+ parser_single.add_argument("--frequency", action="store",
+ help=("frequency of the module"),
+ required=False,
+ choices=list(FREQ_SHORT_NAMES.keys()))
+ parser_single.add_argument("--report", action="store_true",
+ help="enable reporting",
+ required=False)
+ parser_single.add_argument("module_args", nargs="*",
+ metavar='argument',
+ help=('any additional arguments to'
+ ' pass to this module'))
+ parser_single.set_defaults(action=('single', main_single))
+
+ args = parser.parse_args(args=sysv_args)
+
+ try:
+ (name, functor) = args.action
+ except AttributeError:
+ parser.error('too few arguments')
+
+ # Setup basic logging to start (until reinitialized)
+ # iff in debug mode...
+ if args.debug:
+ logging.setupBasicLogging()
+
+ # Setup signal handlers before running
+ signal_handler.attach_handlers()
+
+ if name in ("modules", "init"):
+ functor = status_wrapper
+
+ report_on = True
+ if name == "init":
+ if args.local:
+ rname, rdesc = ("init-local", "searching for local datasources")
+ else:
+ rname, rdesc = ("init-network",
+ "searching for network datasources")
+ elif name == "modules":
+ rname, rdesc = ("modules-%s" % args.mode,
+ "running modules for %s" % args.mode)
+ elif name == "single":
+ rname, rdesc = ("single/%s" % args.name,
+ "running single module %s" % args.name)
+ report_on = args.report
+
+ args.reporter = events.ReportEventStack(
+ rname, rdesc, reporting_enabled=report_on)
+ with args.reporter:
+ return util.log_time(
+ logfunc=LOG.debug, msg="cloud-init mode '%s'" % name,
+ get_uptime=True, func=functor, args=(name, args))
diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py
index e3fadc12..05ad4b03 100644
--- a/cloudinit/config/cc_apt_configure.py
+++ b/cloudinit/config/cc_apt_configure.py
@@ -22,6 +22,7 @@ import glob
import os
import re
+from cloudinit import gpg
from cloudinit import templater
from cloudinit import util
@@ -34,21 +35,6 @@ APT_PROXY_FN = "/etc/apt/apt.conf.d/95cloud-init-proxy"
# this will match 'XXX:YYY' (ie, 'cloud-archive:foo' or 'ppa:bar')
ADD_APT_REPO_MATCH = r"^[\w-]+:\w"
-# A temporary shell program to get a given gpg key
-# from a given keyserver
-EXPORT_GPG_KEYID = """
- k=${1} ks=${2};
- exec 2>/dev/null
- [ -n "$k" ] || exit 1;
- armour=$(gpg --list-keys --armour "${k}")
- if [ -z "${armour}" ]; then
- gpg --keyserver ${ks} --recv $k >/dev/null &&
- armour=$(gpg --export --armour "${k}") &&
- gpg --batch --yes --delete-keys "${k}"
- fi
- [ -n "${armour}" ] && echo "${armour}"
-"""
-
def handle(name, cfg, cloud, log, _args):
if util.is_false(cfg.get('apt_configure_enabled', True)):
@@ -70,7 +56,7 @@ def handle(name, cfg, cloud, log, _args):
if not util.get_cfg_option_bool(cfg,
'apt_preserve_sources_list', False):
- generate_sources_list(release, mirrors, cloud, log)
+ generate_sources_list(cfg, release, mirrors, cloud, log)
old_mirrors = cfg.get('apt_old_mirrors',
{"primary": "archive.ubuntu.com/ubuntu",
"security": "security.ubuntu.com/ubuntu"})
@@ -94,8 +80,8 @@ def handle(name, cfg, cloud, log, _args):
def matcher(x):
return False
- errors = add_sources(cfg['apt_sources'], params,
- aa_repo_match=matcher)
+ errors = add_apt_sources(cfg['apt_sources'], params,
+ aa_repo_match=matcher)
for e in errors:
log.warn("Add source error: %s", ':'.join(e))
@@ -108,17 +94,7 @@ def handle(name, cfg, cloud, log, _args):
util.logexc(log, "Failed to run debconf-set-selections")
-# get gpg keyid from keyserver
-def getkeybyid(keyid, keyserver):
- with util.ExtendedTemporaryFile(suffix='.sh', mode="w+", ) as fh:
- fh.write(EXPORT_GPG_KEYID)
- fh.flush()
- cmd = ['/bin/sh', fh.name, keyid, keyserver]
- (stdout, _stderr) = util.subp(cmd)
- return stdout.strip()
-
-
-def mirror2lists_fileprefix(mirror):
+def mirrorurl_to_apt_fileprefix(mirror):
string = mirror
# take off http:// or ftp://
if string.endswith("/"):
@@ -135,8 +111,8 @@ def rename_apt_lists(old_mirrors, new_mirrors, lists_d="/var/lib/apt/lists"):
nmirror = new_mirrors.get(name)
if not nmirror:
continue
- oprefix = os.path.join(lists_d, mirror2lists_fileprefix(omirror))
- nprefix = os.path.join(lists_d, mirror2lists_fileprefix(nmirror))
+ oprefix = os.path.join(lists_d, mirrorurl_to_apt_fileprefix(omirror))
+ nprefix = os.path.join(lists_d, mirrorurl_to_apt_fileprefix(nmirror))
if oprefix == nprefix:
continue
olen = len(oprefix)
@@ -149,7 +125,17 @@ def get_release():
return stdout.strip()
-def generate_sources_list(codename, mirrors, cloud, log):
+def generate_sources_list(cfg, codename, mirrors, cloud, log):
+ params = {'codename': codename}
+ for k in mirrors:
+ params[k] = mirrors[k]
+
+ custtmpl = cfg.get('apt_custom_sources_list', None)
+ if custtmpl is not None:
+ templater.render_string_to_file(custtmpl,
+ '/etc/apt/sources.list', params)
+ return
+
template_fn = cloud.get_template_filename('sources.list.%s' %
(cloud.distro.name))
if not template_fn:
@@ -158,13 +144,61 @@ def generate_sources_list(codename, mirrors, cloud, log):
log.warn("No template found, not rendering /etc/apt/sources.list")
return
- params = {'codename': codename}
- for k in mirrors:
- params[k] = mirrors[k]
templater.render_to_file(template_fn, '/etc/apt/sources.list', params)
-def add_sources(srclist, template_params=None, aa_repo_match=None):
+def add_apt_key_raw(key):
+ """
+ actual adding of a key as defined in key argument
+ to the system
+ """
+ try:
+ util.subp(('apt-key', 'add', '-'), key)
+ except util.ProcessExecutionError:
+ raise ValueError('failed to add apt GPG Key to apt keyring')
+
+
+def add_apt_key(ent):
+ """
+ add key to the system as defined in ent (if any)
+ supports raw keys or keyid's
+ The latter will as a first step fetch the raw key from a keyserver
+ """
+ if 'keyid' in ent and 'key' not in ent:
+ keyserver = "keyserver.ubuntu.com"
+ if 'keyserver' in ent:
+ keyserver = ent['keyserver']
+ ent['key'] = gpg.get_key_by_id(ent['keyid'], keyserver)
+
+ if 'key' in ent:
+ add_apt_key_raw(ent['key'])
+
+
+def convert_to_new_format(srclist):
+ """convert_to_new_format
+ convert the old list based format to the new dict based one
+ """
+ srcdict = {}
+ if isinstance(srclist, list):
+ for srcent in srclist:
+ if 'filename' not in srcent:
+ # file collides for multiple !filename cases for compatibility
+ # yet we need them all processed, so not same dictionary key
+ srcent['filename'] = "cloud_config_sources.list"
+ key = util.rand_dict_key(srcdict, "cloud_config_sources.list")
+ else:
+ # all with filename use that as key (matching new format)
+ key = srcent['filename']
+ srcdict[key] = srcent
+ elif isinstance(srclist, dict):
+ srcdict = srclist
+ else:
+ raise ValueError("unknown apt_sources format")
+
+ return srcdict
+
+
+def add_apt_sources(srclist, template_params=None, aa_repo_match=None):
"""
add entries in /etc/apt/sources.list.d for each abbreviated
sources.list entry in 'srclist'. When rendering template, also
@@ -174,18 +208,34 @@ def add_sources(srclist, template_params=None, aa_repo_match=None):
template_params = {}
if aa_repo_match is None:
- def aa_repo_match(x):
+ def _aa_repo_match(x):
return False
+ aa_repo_match = _aa_repo_match
errorlist = []
- for ent in srclist:
+ srcdict = convert_to_new_format(srclist)
+
+ for filename in srcdict:
+ ent = srcdict[filename]
+ if 'filename' not in ent:
+ ent['filename'] = filename
+
+ # keys can be added without specifying a source
+ try:
+ add_apt_key(ent)
+ except ValueError as detail:
+ errorlist.append([ent, detail])
+
if 'source' not in ent:
errorlist.append(["", "missing source"])
continue
-
source = ent['source']
source = templater.render_string(source, template_params)
+ if not ent['filename'].startswith(os.path.sep):
+ ent['filename'] = os.path.join("/etc/apt/sources.list.d/",
+ ent['filename'])
+
if aa_repo_match(source):
try:
util.subp(["add-apt-repository", source])
@@ -194,29 +244,6 @@ def add_sources(srclist, template_params=None, aa_repo_match=None):
("add-apt-repository failed. " + str(e))])
continue
- if 'filename' not in ent:
- ent['filename'] = 'cloud_config_sources.list'
-
- if not ent['filename'].startswith("/"):
- ent['filename'] = os.path.join("/etc/apt/sources.list.d/",
- ent['filename'])
-
- if ('keyid' in ent and 'key' not in ent):
- ks = "keyserver.ubuntu.com"
- if 'keyserver' in ent:
- ks = ent['keyserver']
- try:
- ent['key'] = getkeybyid(ent['keyid'], ks)
- except Exception:
- errorlist.append([source, "failed to get key from %s" % ks])
- continue
-
- if 'key' in ent:
- try:
- util.subp(('apt-key', 'add', '-'), ent['key'])
- except Exception:
- errorlist.append([source, "failed add key"])
-
try:
contents = "%s\n" % (source)
util.write_file(ent['filename'], contents, omode="ab")
diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py
index 859d69f1..40560f11 100644
--- a/cloudinit/config/cc_growpart.py
+++ b/cloudinit/config/cc_growpart.py
@@ -36,13 +36,13 @@ DEFAULT_CONFIG = {
}
-def enum(**enums):
- return type('Enum', (), enums)
+class RESIZE(object):
+ SKIPPED = "SKIPPED"
+ CHANGED = "CHANGED"
+ NOCHANGE = "NOCHANGE"
+ FAILED = "FAILED"
-RESIZE = enum(SKIPPED="SKIPPED", CHANGED="CHANGED", NOCHANGE="NOCHANGE",
- FAILED="FAILED")
-
LOG = logging.getLogger(__name__)
diff --git a/cloudinit/cs_utils.py b/cloudinit/cs_utils.py
index 83ac1a0e..412431f2 100644
--- a/cloudinit/cs_utils.py
+++ b/cloudinit/cs_utils.py
@@ -33,7 +33,8 @@ API Docs: http://cloudsigma-docs.readthedocs.org/en/latest/server_context.html
import json
import platform
-import serial
+from cloudinit import serial
+
# these high timeouts are necessary as read may read a lot of data.
READ_TIMEOUT = 60
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 5c29c804..14b500f8 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -454,7 +454,7 @@ class Distro(object):
keys = kwargs['ssh_authorized_keys']
if isinstance(keys, six.string_types):
keys = [keys]
- if isinstance(keys, dict):
+ elif isinstance(keys, dict):
keys = list(keys.values())
if keys is not None:
if not isinstance(keys, (tuple, list, set)):
diff --git a/cloudinit/distros/arch.py b/cloudinit/distros/arch.py
index 93a2e008..66209f22 100644
--- a/cloudinit/distros/arch.py
+++ b/cloudinit/distros/arch.py
@@ -196,6 +196,6 @@ def convert_resolv_conf(settings):
"""Returns a settings string formatted for resolv.conf."""
result = ''
if isinstance(settings, list):
- for ns in list:
+ for ns in settings:
result = result + 'nameserver %s\n' % ns
- return result
+ return result
diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py
index 32bef1cd..53f3aa4d 100644
--- a/cloudinit/distros/debian.py
+++ b/cloudinit/distros/debian.py
@@ -25,7 +25,8 @@ import os
from cloudinit import distros
from cloudinit import helpers
from cloudinit import log as logging
-from cloudinit import net
+from cloudinit.net import eni
+from cloudinit.net.network_state import parse_net_config_data
from cloudinit import util
from cloudinit.distros.parsers.hostname import HostnameConf
@@ -56,6 +57,11 @@ class Distro(distros.Distro):
# should only happen say once per instance...)
self._runner = helpers.Runners(paths)
self.osfamily = 'debian'
+ self._net_renderer = eni.Renderer({
+ 'eni_path': self.network_conf_fn,
+ 'links_prefix_path': self.links_prefix,
+ 'netrules_path': None,
+ })
def apply_locale(self, locale, out_fn=None):
if not out_fn:
@@ -79,13 +85,9 @@ class Distro(distros.Distro):
return ['all']
def _write_network_config(self, netconfig):
- ns = net.parse_net_config_data(netconfig)
- net.render_network_state(target="/", network_state=ns,
- eni=self.network_conf_fn,
- links_prefix=self.links_prefix,
- netrules=None)
+ ns = parse_net_config_data(netconfig)
+ self._net_renderer.render_network_state("/", ns)
_maybe_remove_legacy_eth0()
-
return []
def _bring_up_interfaces(self, device_names):
diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py
index 812e7002..1aa42d75 100644
--- a/cloudinit/distros/rhel.py
+++ b/cloudinit/distros/rhel.py
@@ -23,6 +23,8 @@
from cloudinit import distros
from cloudinit import helpers
from cloudinit import log as logging
+from cloudinit.net.network_state import parse_net_config_data
+from cloudinit.net import sysconfig
from cloudinit import util
from cloudinit.distros import net_util
@@ -59,10 +61,16 @@ class Distro(distros.Distro):
# should only happen say once per instance...)
self._runner = helpers.Runners(paths)
self.osfamily = 'redhat'
+ self._net_renderer = sysconfig.Renderer()
def install_packages(self, pkglist):
self.package_command('install', pkgs=pkglist)
+ def _write_network_config(self, netconfig):
+ ns = parse_net_config_data(netconfig)
+ self._net_renderer.render_network_state("/", ns)
+ return []
+
def _write_network(self, settings):
# TODO(harlowja) fix this... since this is the ubuntu format
entries = net_util.translate_network(settings)
diff --git a/cloudinit/gpg.py b/cloudinit/gpg.py
new file mode 100644
index 00000000..6a76d785
--- /dev/null
+++ b/cloudinit/gpg.py
@@ -0,0 +1,74 @@
+"""gpg.py - Collection of gpg key related functions"""
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2016 Canonical Ltd.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Christian Ehrhardt <christian.ehrhardt@canonical.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/>.
+
+from cloudinit import log as logging
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+
+
+def export_armour(key):
+ """Export gpg key, armoured key gets returned"""
+ try:
+ (armour, _) = util.subp(["gpg", "--export", "--armour", key],
+ capture=True)
+ except util.ProcessExecutionError as error:
+ # debug, since it happens for any key not on the system initially
+ LOG.debug('Failed to export armoured key "%s": %s', key, error)
+ armour = None
+ return armour
+
+
+def receive_key(key, keyserver):
+ """Receive gpg key from the specified keyserver"""
+ LOG.debug('Receive gpg key "%s"', key)
+ try:
+ util.subp(["gpg", "--keyserver", keyserver, "--recv-keys", key],
+ capture=True)
+ except util.ProcessExecutionError as error:
+ raise ValueError(('Failed to import key "%s" '
+ 'from server "%s" - error %s') %
+ (key, keyserver, error))
+
+
+def delete_key(key):
+ """Delete the specified key from the local gpg ring"""
+ try:
+ util.subp(["gpg", "--batch", "--yes", "--delete-keys", key],
+ capture=True)
+ except util.ProcessExecutionError as error:
+ LOG.warn('Failed delete key "%s": %s', key, error)
+
+
+def get_key_by_id(keyid, keyserver="keyserver.ubuntu.com"):
+ """get gpg keyid from keyserver"""
+ armour = export_armour(keyid)
+ if not armour:
+ try:
+ receive_key(keyid, keyserver=keyserver)
+ armour = export_armour(keyid)
+ except ValueError:
+ LOG.exception('Failed to obtain gpg key %s', keyid)
+ raise
+ finally:
+ # delete just imported key to leave environment as it was before
+ delete_key(keyid)
+
+ return armour
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
index 0066561e..6959ad34 100644
--- a/cloudinit/net/__init__.py
+++ b/cloudinit/net/__init__.py
@@ -16,42 +16,15 @@
# You should have received a copy of the GNU Affero General Public License
# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
-import base64
-import copy
import errno
-import glob
-import gzip
-import io
+import logging
import os
import re
-import shlex
-from cloudinit import log as logging
-from cloudinit.net import network_state
-from cloudinit.net.udev import generate_udev_rule
from cloudinit import util
LOG = logging.getLogger(__name__)
-
SYS_CLASS_NET = "/sys/class/net/"
-LINKS_FNAME_PREFIX = "etc/systemd/network/50-cloud-init-"
-
-NET_CONFIG_OPTIONS = [
- "address", "netmask", "broadcast", "network", "metric", "gateway",
- "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime",
- "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame",
- "netnum", "endpoint", "local", "ttl",
-]
-
-NET_CONFIG_COMMANDS = [
- "pre-up", "up", "post-up", "down", "pre-down", "post-down",
-]
-
-NET_CONFIG_BRIDGE_OPTIONS = [
- "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit",
- "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp",
-]
-
DEFAULT_PRIMARY_INTERFACE = 'eth0'
@@ -61,23 +34,22 @@ def sys_dev_path(devname, path=""):
def read_sys_net(devname, path, translate=None, enoent=None, keyerror=None):
try:
- contents = ""
- with open(sys_dev_path(devname, path), "r") as fp:
- contents = fp.read().strip()
- if translate is None:
- return contents
-
- try:
- return translate.get(contents)
- except KeyError:
- LOG.debug("found unexpected value '%s' in '%s/%s'", contents,
- devname, path)
- if keyerror is not None:
- return keyerror
- raise
- except OSError as e:
- if e.errno == errno.ENOENT and enoent is not None:
- return enoent
+ contents = util.load_file(sys_dev_path(devname, path))
+ except (OSError, IOError) as e:
+ if getattr(e, 'errno', None) == errno.ENOENT:
+ if enoent is not None:
+ return enoent
+ raise
+ contents = contents.strip()
+ if translate is None:
+ return contents
+ try:
+ return translate.get(contents)
+ except KeyError:
+ LOG.debug("found unexpected value '%s' in '%s/%s'", contents,
+ devname, path)
+ if keyerror is not None:
+ return keyerror
raise
@@ -128,523 +100,7 @@ def get_devicelist():
class ParserError(Exception):
- """Raised when parser has issue parsing the interfaces file."""
-
-
-def parse_deb_config_data(ifaces, contents, src_dir, src_path):
- """Parses the file contents, placing result into ifaces.
-
- '_source_path' is added to every dictionary entry to define which file
- the configration information came from.
-
- :param ifaces: interface dictionary
- :param contents: contents of interfaces file
- :param src_dir: directory interfaces file was located
- :param src_path: file path the `contents` was read
- """
- currif = None
- for line in contents.splitlines():
- line = line.strip()
- if line.startswith('#'):
- continue
- split = line.split(' ')
- option = split[0]
- if option == "source-directory":
- parsed_src_dir = split[1]
- if not parsed_src_dir.startswith("/"):
- parsed_src_dir = os.path.join(src_dir, parsed_src_dir)
- for expanded_path in glob.glob(parsed_src_dir):
- dir_contents = os.listdir(expanded_path)
- dir_contents = [
- os.path.join(expanded_path, path)
- for path in dir_contents
- if (os.path.isfile(os.path.join(expanded_path, path)) and
- re.match("^[a-zA-Z0-9_-]+$", path) is not None)
- ]
- for entry in dir_contents:
- with open(entry, "r") as fp:
- src_data = fp.read().strip()
- abs_entry = os.path.abspath(entry)
- parse_deb_config_data(
- ifaces, src_data,
- os.path.dirname(abs_entry), abs_entry)
- elif option == "source":
- new_src_path = split[1]
- if not new_src_path.startswith("/"):
- new_src_path = os.path.join(src_dir, new_src_path)
- for expanded_path in glob.glob(new_src_path):
- with open(expanded_path, "r") as fp:
- src_data = fp.read().strip()
- abs_path = os.path.abspath(expanded_path)
- parse_deb_config_data(
- ifaces, src_data,
- os.path.dirname(abs_path), abs_path)
- elif option == "auto":
- for iface in split[1:]:
- if iface not in ifaces:
- ifaces[iface] = {
- # Include the source path this interface was found in.
- "_source_path": src_path
- }
- ifaces[iface]['auto'] = True
- elif option == "iface":
- iface, family, method = split[1:4]
- if iface not in ifaces:
- ifaces[iface] = {
- # Include the source path this interface was found in.
- "_source_path": src_path
- }
- elif 'family' in ifaces[iface]:
- raise ParserError(
- "Interface %s can only be defined once. "
- "Re-defined in '%s'." % (iface, src_path))
- ifaces[iface]['family'] = family
- ifaces[iface]['method'] = method
- currif = iface
- elif option == "hwaddress":
- if split[1] == "ether":
- val = split[2]
- else:
- val = split[1]
- ifaces[currif]['hwaddress'] = val
- elif option in NET_CONFIG_OPTIONS:
- ifaces[currif][option] = split[1]
- elif option in NET_CONFIG_COMMANDS:
- if option not in ifaces[currif]:
- ifaces[currif][option] = []
- ifaces[currif][option].append(' '.join(split[1:]))
- elif option.startswith('dns-'):
- if 'dns' not in ifaces[currif]:
- ifaces[currif]['dns'] = {}
- if option == 'dns-search':
- ifaces[currif]['dns']['search'] = []
- for domain in split[1:]:
- ifaces[currif]['dns']['search'].append(domain)
- elif option == 'dns-nameservers':
- ifaces[currif]['dns']['nameservers'] = []
- for server in split[1:]:
- ifaces[currif]['dns']['nameservers'].append(server)
- elif option.startswith('bridge_'):
- if 'bridge' not in ifaces[currif]:
- ifaces[currif]['bridge'] = {}
- if option in NET_CONFIG_BRIDGE_OPTIONS:
- bridge_option = option.replace('bridge_', '', 1)
- ifaces[currif]['bridge'][bridge_option] = split[1]
- elif option == "bridge_ports":
- ifaces[currif]['bridge']['ports'] = []
- for iface in split[1:]:
- ifaces[currif]['bridge']['ports'].append(iface)
- elif option == "bridge_hw" and split[1].lower() == "mac":
- ifaces[currif]['bridge']['mac'] = split[2]
- elif option == "bridge_pathcost":
- if 'pathcost' not in ifaces[currif]['bridge']:
- ifaces[currif]['bridge']['pathcost'] = {}
- ifaces[currif]['bridge']['pathcost'][split[1]] = split[2]
- elif option == "bridge_portprio":
- if 'portprio' not in ifaces[currif]['bridge']:
- ifaces[currif]['bridge']['portprio'] = {}
- ifaces[currif]['bridge']['portprio'][split[1]] = split[2]
- elif option.startswith('bond-'):
- if 'bond' not in ifaces[currif]:
- ifaces[currif]['bond'] = {}
- bond_option = option.replace('bond-', '', 1)
- ifaces[currif]['bond'][bond_option] = split[1]
- for iface in ifaces.keys():
- if 'auto' not in ifaces[iface]:
- ifaces[iface]['auto'] = False
-
-
-def parse_deb_config(path):
- """Parses a debian network configuration file."""
- ifaces = {}
- with open(path, "r") as fp:
- contents = fp.read().strip()
- abs_path = os.path.abspath(path)
- parse_deb_config_data(
- ifaces, contents,
- os.path.dirname(abs_path), abs_path)
- return ifaces
-
-
-def parse_net_config_data(net_config):
- """Parses the config, returns NetworkState dictionary
-
- :param net_config: curtin network config dict
- """
- state = None
- if 'version' in net_config and 'config' in net_config:
- ns = network_state.NetworkState(version=net_config.get('version'),
- config=net_config.get('config'))
- ns.parse_config()
- state = ns.network_state
-
- return state
-
-
-def parse_net_config(path):
- """Parses a curtin network configuration file and
- return network state"""
- ns = None
- net_config = util.read_conf(path)
- if 'network' in net_config:
- ns = parse_net_config_data(net_config.get('network'))
-
- return ns
-
-
-def _load_shell_content(content, add_empty=False, empty_val=None):
- """Given shell like syntax (key=value\nkey2=value2\n) in content
- return the data in dictionary form. If 'add_empty' is True
- then add entries in to the returned dictionary for 'VAR='
- variables. Set their value to empty_val."""
- data = {}
- for line in shlex.split(content):
- key, value = line.split("=", 1)
- if not value:
- value = empty_val
- if add_empty or value:
- data[key] = value
-
- return data
-
-
-def _klibc_to_config_entry(content, mac_addrs=None):
- """Convert a klibc writtent shell content file to a 'config' entry
- When ip= is seen on the kernel command line in debian initramfs
- and networking is brought up, ipconfig will populate
- /run/net-<name>.cfg.
-
- The files are shell style syntax, and examples are in the tests
- provided here. There is no good documentation on this unfortunately.
-
- DEVICE=<name> is expected/required and PROTO should indicate if
- this is 'static' or 'dhcp'.
- """
-
- if mac_addrs is None:
- mac_addrs = {}
-
- data = _load_shell_content(content)
- try:
- name = data['DEVICE']
- except KeyError:
- raise ValueError("no 'DEVICE' entry in data")
-
- # ipconfig on precise does not write PROTO
- proto = data.get('PROTO')
- if not proto:
- if data.get('filename'):
- proto = 'dhcp'
- else:
- proto = 'static'
-
- if proto not in ('static', 'dhcp'):
- raise ValueError("Unexpected value for PROTO: %s" % proto)
-
- iface = {
- 'type': 'physical',
- 'name': name,
- 'subnets': [],
- }
-
- if name in mac_addrs:
- iface['mac_address'] = mac_addrs[name]
-
- # originally believed there might be IPV6* values
- for v, pre in (('ipv4', 'IPV4'),):
- # if no IPV4ADDR or IPV6ADDR, then go on.
- if pre + "ADDR" not in data:
- continue
- subnet = {'type': proto, 'control': 'manual'}
-
- # these fields go right on the subnet
- for key in ('NETMASK', 'BROADCAST', 'GATEWAY'):
- if pre + key in data:
- subnet[key.lower()] = data[pre + key]
-
- dns = []
- # handle IPV4DNS0 or IPV6DNS0
- for nskey in ('DNS0', 'DNS1'):
- ns = data.get(pre + nskey)
- # verify it has something other than 0.0.0.0 (or ipv6)
- if ns and len(ns.strip(":.0")):
- dns.append(data[pre + nskey])
- if dns:
- subnet['dns_nameservers'] = dns
- # add search to both ipv4 and ipv6, as it has no namespace
- search = data.get('DOMAINSEARCH')
- if search:
- if ',' in search:
- subnet['dns_search'] = search.split(",")
- else:
- subnet['dns_search'] = search.split()
-
- iface['subnets'].append(subnet)
-
- return name, iface
-
-
-def config_from_klibc_net_cfg(files=None, mac_addrs=None):
- if files is None:
- files = glob.glob('/run/net*.conf')
-
- entries = []
- names = {}
- for cfg_file in files:
- name, entry = _klibc_to_config_entry(util.load_file(cfg_file),
- mac_addrs=mac_addrs)
- if name in names:
- raise ValueError(
- "device '%s' defined multiple times: %s and %s" % (
- name, names[name], cfg_file))
-
- names[name] = cfg_file
- entries.append(entry)
- return {'config': entries, 'version': 1}
-
-
-def render_persistent_net(network_state):
- '''Given state, emit udev rules to map mac to ifname.'''
- content = ""
- interfaces = network_state.get('interfaces')
- for iface in interfaces.values():
- # for physical interfaces write out a persist net udev rule
- if iface['type'] == 'physical' and \
- 'name' in iface and iface.get('mac_address'):
- content += generate_udev_rule(iface['name'],
- iface['mac_address'])
-
- return content
-
-
-# TODO: switch valid_map based on mode inet/inet6
-def iface_add_subnet(iface, subnet):
- content = []
- valid_map = [
- 'address',
- 'netmask',
- 'broadcast',
- 'metric',
- 'gateway',
- 'pointopoint',
- 'mtu',
- 'scope',
- 'dns_search',
- 'dns_nameservers',
- ]
- for key, value in subnet.items():
- if value and key in valid_map:
- if type(value) == list:
- value = " ".join(value)
- if '_' in key:
- key = key.replace('_', '-')
- content.append(" {} {}".format(key, value))
-
- return content
-
-
-# TODO: switch to valid_map for attrs
-def iface_add_attrs(iface):
- content = []
- ignore_map = [
- 'control',
- 'index',
- 'inet',
- 'mode',
- 'name',
- 'subnets',
- 'type',
- ]
- if iface['type'] not in ['bond', 'bridge', 'vlan']:
- ignore_map.append('mac_address')
-
- for key, value in iface.items():
- if value and key not in ignore_map:
- if type(value) == list:
- value = " ".join(value)
- content.append(" {} {}".format(key, value))
-
- return content
-
-
-def render_route(route, indent=""):
- """When rendering routes for an iface, in some cases applying a route
- may result in the route command returning non-zero which produces
- some confusing output for users manually using ifup/ifdown[1]. To
- that end, we will optionally include an '|| true' postfix to each
- route line allowing users to work with ifup/ifdown without using
- --force option.
-
- We may at somepoint not want to emit this additional postfix, and
- add a 'strict' flag to this function. When called with strict=True,
- then we will not append the postfix.
-
- 1. http://askubuntu.com/questions/168033/
- how-to-set-static-routes-in-ubuntu-server
- """
- content = []
- up = indent + "post-up route add"
- down = indent + "pre-down route del"
- eol = " || true"
- mapping = {
- 'network': '-net',
- 'netmask': 'netmask',
- 'gateway': 'gw',
- 'metric': 'metric',
- }
- if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
- default_gw = " default gw %s" % route['gateway']
- content.append(up + default_gw + eol)
- content.append(down + default_gw + eol)
- elif route['network'] == '::' and route['netmask'] == 0:
- # ipv6!
- default_gw = " -A inet6 default gw %s" % route['gateway']
- content.append(up + default_gw + eol)
- content.append(down + default_gw + eol)
- else:
- route_line = ""
- for k in ['network', 'netmask', 'gateway', 'metric']:
- if k in route:
- route_line += " %s %s" % (mapping[k], route[k])
- content.append(up + route_line + eol)
- content.append(down + route_line + eol)
-
- return content
-
-
-def iface_start_entry(iface, index):
- fullname = iface['name']
- if index != 0:
- fullname += ":%s" % index
-
- control = iface['control']
- if control == "auto":
- cverb = "auto"
- elif control in ("hotplug",):
- cverb = "allow-" + control
- else:
- cverb = "# control-" + control
-
- subst = iface.copy()
- subst.update({'fullname': fullname, 'cverb': cverb})
-
- if 'inet' not in subst:
- print("bug....iface: %s" % iface)
- return ["{cverb} {fullname}".format(**subst),
- "iface {fullname} {inet} {mode}".format(**subst)]
-
-
-def _render_iface(iface):
- lines = []
- subnets = iface.get('subnets', {})
- if subnets:
- for index, subnet in zip(range(0, len(subnets)), subnets):
- iface['index'] = index
- iface['mode'] = subnet['type']
- iface['control'] = subnet.get('control', 'auto')
- if iface['mode'].endswith('6'):
- iface['inet'] += '6'
- elif iface['mode'] == 'static' and ":" in subnet['address']:
- iface['inet'] += '6'
- if iface['mode'].startswith('dhcp'):
- iface['mode'] = 'dhcp'
-
- lines.extend(iface_start_entry(iface, index))
- lines.extend(iface_add_subnet(iface, subnet))
- lines.extend(iface_add_attrs(iface))
- lines.append("")
- else:
- # ifenslave docs say to auto the slave devices
- if 'bond-master' in iface:
- lines.append("auto {name}".format(**iface))
- lines.append("iface {name} {inet} {mode}".format(**iface))
- lines.extend(iface_add_attrs(iface))
- return lines
-
-
-def render_interfaces(network_state):
- '''Given state, emit etc/network/interfaces content.'''
-
- content = ""
- interfaces = network_state.get('interfaces')
- ''' Apply a sort order to ensure that we write out
- the physical interfaces first; this is critical for
- bonding
- '''
- order = {
- 'physical': 0,
- 'bond': 1,
- 'bridge': 2,
- 'vlan': 3,
- }
-
- # handle 'lo' specifically as we need to insert the global dns entries
- # there (as that is the only interface) that will be always up.
- lo = {'name': 'lo', 'type': 'physical', 'inet': 'inet',
- 'subnets': [{'type': 'loopback', 'control': 'auto'}]}
- for iface in interfaces.values():
- if iface.get('name') == "lo":
- lo = copy.deepcopy(iface)
- for dnskey, value in network_state.get('dns', {}).items():
- if len(value):
- lo['subnets'][0]["dns_" + dnskey] = value
-
- sections = [_render_iface(lo)]
-
- for iface in sorted(interfaces.values(),
- key=lambda k: (order[k['type']], k['name'])):
- if iface.get('name') == "lo":
- continue
- sections.append(_render_iface(iface))
-
- for route in network_state.get('routes'):
- sections.append(render_route(route))
-
- content = ''.join(['\n'.join(s) + '\n\n' for s in sections])
- # global replacements until v2 format
- content = content.replace('mac_address', 'hwaddress')
- return content
-
-
-def render_network_state(target, network_state, eni="etc/network/interfaces",
- links_prefix=LINKS_FNAME_PREFIX,
- netrules='etc/udev/rules.d/70-persistent-net.rules'):
-
- fpeni = os.path.sep.join((target, eni,))
- util.ensure_dir(os.path.dirname(fpeni))
- with open(fpeni, 'w+') as f:
- f.write(render_interfaces(network_state))
-
- if netrules:
- netrules = os.path.sep.join((target, netrules,))
- util.ensure_dir(os.path.dirname(netrules))
- with open(netrules, 'w+') as f:
- f.write(render_persistent_net(network_state))
-
- if links_prefix:
- render_systemd_links(target, network_state, links_prefix)
-
-
-def render_systemd_links(target, network_state,
- links_prefix=LINKS_FNAME_PREFIX):
- fp_prefix = os.path.sep.join((target, links_prefix))
- for f in glob.glob(fp_prefix + "*"):
- os.unlink(f)
-
- interfaces = network_state.get('interfaces')
- for iface in interfaces.values():
- if (iface['type'] == 'physical' and 'name' in iface and
- iface.get('mac_address')):
- fname = fp_prefix + iface['name'] + ".link"
- with open(fname, "w") as fp:
- fp.write("\n".join([
- "[Match]",
- "MACAddress=" + iface['mac_address'],
- "",
- "[Link]",
- "Name=" + iface['name'],
- ""
- ]))
+ """Raised when a parser has issue parsing a file/content."""
def is_disabled_cfg(cfg):
@@ -657,7 +113,6 @@ def sys_netdev_info(name, field):
if not os.path.exists(os.path.join(SYS_CLASS_NET, name)):
raise OSError("%s: interface does not exist in %s" %
(name, SYS_CLASS_NET))
-
fname = os.path.join(SYS_CLASS_NET, name, field)
if not os.path.exists(fname):
raise OSError("%s: could not find sysfs entry: %s" % (name, fname))
@@ -737,104 +192,6 @@ def generate_fallback_config():
return nconf
-def _decomp_gzip(blob, strict=True):
- # decompress blob. raise exception if not compressed unless strict=False.
- with io.BytesIO(blob) as iobuf:
- gzfp = None
- try:
- gzfp = gzip.GzipFile(mode="rb", fileobj=iobuf)
- return gzfp.read()
- except IOError:
- if strict:
- raise
- return blob
- finally:
- if gzfp:
- gzfp.close()
-
-
-def _b64dgz(b64str, gzipped="try"):
- # decode a base64 string. If gzipped is true, transparently uncompresss
- # if gzipped is 'try', then try gunzip, returning the original on fail.
- try:
- blob = base64.b64decode(b64str)
- except TypeError:
- raise ValueError("Invalid base64 text: %s" % b64str)
-
- if not gzipped:
- return blob
-
- return _decomp_gzip(blob, strict=gzipped != "try")
-
-
-def read_kernel_cmdline_config(files=None, mac_addrs=None, cmdline=None):
- if cmdline is None:
- cmdline = util.get_cmdline()
-
- if 'network-config=' in cmdline:
- data64 = None
- for tok in cmdline.split():
- if tok.startswith("network-config="):
- data64 = tok.split("=", 1)[1]
- if data64:
- return util.load_yaml(_b64dgz(data64))
-
- if 'ip=' not in cmdline:
- return None
-
- if mac_addrs is None:
- mac_addrs = {k: sys_netdev_info(k, 'address')
- for k in get_devicelist()}
-
- return config_from_klibc_net_cfg(files=files, mac_addrs=mac_addrs)
-
-
-def convert_eni_data(eni_data):
- # return a network config representation of what is in eni_data
- ifaces = {}
- parse_deb_config_data(ifaces, eni_data, src_dir=None, src_path=None)
- return _ifaces_to_net_config_data(ifaces)
-
-
-def _ifaces_to_net_config_data(ifaces):
- """Return network config that represents the ifaces data provided.
- ifaces = parse_deb_config("/etc/network/interfaces")
- config = ifaces_to_net_config_data(ifaces)
- state = parse_net_config_data(config)."""
- devs = {}
- for name, data in ifaces.items():
- # devname is 'eth0' for name='eth0:1'
- devname = name.partition(":")[0]
- if devname not in devs:
- devs[devname] = {'type': 'physical', 'name': devname,
- 'subnets': []}
- # this isnt strictly correct, but some might specify
- # hwaddress on a nic for matching / declaring name.
- if 'hwaddress' in data:
- devs[devname]['mac_address'] = data['hwaddress']
- subnet = {'_orig_eni_name': name, 'type': data['method']}
- if data.get('auto'):
- subnet['control'] = 'auto'
- else:
- subnet['control'] = 'manual'
-
- if data.get('method') == 'static':
- subnet['address'] = data['address']
-
- for copy_key in ('netmask', 'gateway', 'broadcast'):
- if copy_key in data:
- subnet[copy_key] = data[copy_key]
-
- if 'dns' in data:
- for n in ('nameservers', 'search'):
- if n in data['dns'] and data['dns'][n]:
- subnet['dns_' + n] = data['dns'][n]
- devs[devname]['subnets'].append(subnet)
-
- return {'version': 1,
- 'config': [devs[d] for d in sorted(devs)]}
-
-
def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
"""read the network config and rename devices accordingly.
if strict_present is false, then do not raise exception if no devices
@@ -850,7 +207,7 @@ def apply_network_config_names(netcfg, strict_present=True, strict_busy=True):
continue
renames.append([mac, name])
- return rename_interfaces(renames)
+ return _rename_interfaces(renames)
def _get_current_rename_info(check_downable=True):
@@ -878,8 +235,8 @@ def _get_current_rename_info(check_downable=True):
return bymac
-def rename_interfaces(renames, strict_present=True, strict_busy=True,
- current_info=None):
+def _rename_interfaces(renames, strict_present=True, strict_busy=True,
+ current_info=None):
if current_info is None:
current_info = _get_current_rename_info()
@@ -990,7 +347,13 @@ def get_interface_mac(ifname):
def get_interfaces_by_mac(devs=None):
"""Build a dictionary of tuples {mac: name}"""
if devs is None:
- devs = get_devicelist()
+ try:
+ devs = get_devicelist()
+ except OSError as e:
+ if e.errno == errno.ENOENT:
+ devs = []
+ else:
+ raise
ret = {}
for name in devs:
mac = get_interface_mac(name)
diff --git a/cloudinit/net/cmdline.py b/cloudinit/net/cmdline.py
new file mode 100644
index 00000000..822a020b
--- /dev/null
+++ b/cloudinit/net/cmdline.py
@@ -0,0 +1,203 @@
+# Copyright (C) 2013-2014 Canonical Ltd.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Blake Rouse <blake.rouse@canonical.com>
+#
+# Curtin is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# Curtin 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 Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
+
+import base64
+import glob
+import gzip
+import io
+import shlex
+import sys
+
+import six
+
+from . import get_devicelist
+from . import sys_netdev_info
+
+from cloudinit import util
+
+PY26 = sys.version_info[0:2] == (2, 6)
+
+
+def _shlex_split(blob):
+ if PY26 and isinstance(blob, six.text_type):
+ # Older versions don't support unicode input
+ blob = blob.encode("utf8")
+ return shlex.split(blob)
+
+
+def _load_shell_content(content, add_empty=False, empty_val=None):
+ """Given shell like syntax (key=value\nkey2=value2\n) in content
+ return the data in dictionary form. If 'add_empty' is True
+ then add entries in to the returned dictionary for 'VAR='
+ variables. Set their value to empty_val."""
+ data = {}
+ for line in _shlex_split(content):
+ key, value = line.split("=", 1)
+ if not value:
+ value = empty_val
+ if add_empty or value:
+ data[key] = value
+
+ return data
+
+
+def _klibc_to_config_entry(content, mac_addrs=None):
+ """Convert a klibc writtent shell content file to a 'config' entry
+ When ip= is seen on the kernel command line in debian initramfs
+ and networking is brought up, ipconfig will populate
+ /run/net-<name>.cfg.
+
+ The files are shell style syntax, and examples are in the tests
+ provided here. There is no good documentation on this unfortunately.
+
+ DEVICE=<name> is expected/required and PROTO should indicate if
+ this is 'static' or 'dhcp'.
+ """
+
+ if mac_addrs is None:
+ mac_addrs = {}
+
+ data = _load_shell_content(content)
+ try:
+ name = data['DEVICE']
+ except KeyError:
+ raise ValueError("no 'DEVICE' entry in data")
+
+ # ipconfig on precise does not write PROTO
+ proto = data.get('PROTO')
+ if not proto:
+ if data.get('filename'):
+ proto = 'dhcp'
+ else:
+ proto = 'static'
+
+ if proto not in ('static', 'dhcp'):
+ raise ValueError("Unexpected value for PROTO: %s" % proto)
+
+ iface = {
+ 'type': 'physical',
+ 'name': name,
+ 'subnets': [],
+ }
+
+ if name in mac_addrs:
+ iface['mac_address'] = mac_addrs[name]
+
+ # originally believed there might be IPV6* values
+ for v, pre in (('ipv4', 'IPV4'),):
+ # if no IPV4ADDR or IPV6ADDR, then go on.
+ if pre + "ADDR" not in data:
+ continue
+ subnet = {'type': proto, 'control': 'manual'}
+
+ # these fields go right on the subnet
+ for key in ('NETMASK', 'BROADCAST', 'GATEWAY'):
+ if pre + key in data:
+ subnet[key.lower()] = data[pre + key]
+
+ dns = []
+ # handle IPV4DNS0 or IPV6DNS0
+ for nskey in ('DNS0', 'DNS1'):
+ ns = data.get(pre + nskey)
+ # verify it has something other than 0.0.0.0 (or ipv6)
+ if ns and len(ns.strip(":.0")):
+ dns.append(data[pre + nskey])
+ if dns:
+ subnet['dns_nameservers'] = dns
+ # add search to both ipv4 and ipv6, as it has no namespace
+ search = data.get('DOMAINSEARCH')
+ if search:
+ if ',' in search:
+ subnet['dns_search'] = search.split(",")
+ else:
+ subnet['dns_search'] = search.split()
+
+ iface['subnets'].append(subnet)
+
+ return name, iface
+
+
+def config_from_klibc_net_cfg(files=None, mac_addrs=None):
+ if files is None:
+ files = glob.glob('/run/net*.conf')
+
+ entries = []
+ names = {}
+ for cfg_file in files:
+ name, entry = _klibc_to_config_entry(util.load_file(cfg_file),
+ mac_addrs=mac_addrs)
+ if name in names:
+ raise ValueError(
+ "device '%s' defined multiple times: %s and %s" % (
+ name, names[name], cfg_file))
+
+ names[name] = cfg_file
+ entries.append(entry)
+ return {'config': entries, 'version': 1}
+
+
+def _decomp_gzip(blob, strict=True):
+ # decompress blob. raise exception if not compressed unless strict=False.
+ with io.BytesIO(blob) as iobuf:
+ gzfp = None
+ try:
+ gzfp = gzip.GzipFile(mode="rb", fileobj=iobuf)
+ return gzfp.read()
+ except IOError:
+ if strict:
+ raise
+ return blob
+ finally:
+ if gzfp:
+ gzfp.close()
+
+
+def _b64dgz(b64str, gzipped="try"):
+ # decode a base64 string. If gzipped is true, transparently uncompresss
+ # if gzipped is 'try', then try gunzip, returning the original on fail.
+ try:
+ blob = base64.b64decode(b64str)
+ except TypeError:
+ raise ValueError("Invalid base64 text: %s" % b64str)
+
+ if not gzipped:
+ return blob
+
+ return _decomp_gzip(blob, strict=gzipped != "try")
+
+
+def read_kernel_cmdline_config(files=None, mac_addrs=None, cmdline=None):
+ if cmdline is None:
+ cmdline = util.get_cmdline()
+
+ if 'network-config=' in cmdline:
+ data64 = None
+ for tok in cmdline.split():
+ if tok.startswith("network-config="):
+ data64 = tok.split("=", 1)[1]
+ if data64:
+ return util.load_yaml(_b64dgz(data64))
+
+ if 'ip=' not in cmdline:
+ return None
+
+ if mac_addrs is None:
+ mac_addrs = dict((k, sys_netdev_info(k, 'address'))
+ for k in get_devicelist())
+
+ return config_from_klibc_net_cfg(files=files, mac_addrs=mac_addrs)
diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py
new file mode 100644
index 00000000..5a91fcf2
--- /dev/null
+++ b/cloudinit/net/eni.py
@@ -0,0 +1,460 @@
+# vi: ts=4 expandtab
+#
+# 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 copy
+import glob
+import os
+import re
+
+from . import ParserError
+
+from . import renderer
+
+from cloudinit import util
+
+
+NET_CONFIG_COMMANDS = [
+ "pre-up", "up", "post-up", "down", "pre-down", "post-down",
+]
+
+NET_CONFIG_BRIDGE_OPTIONS = [
+ "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit",
+ "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp",
+]
+
+NET_CONFIG_OPTIONS = [
+ "address", "netmask", "broadcast", "network", "metric", "gateway",
+ "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime",
+ "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame",
+ "netnum", "endpoint", "local", "ttl",
+]
+
+
+# TODO: switch valid_map based on mode inet/inet6
+def _iface_add_subnet(iface, subnet):
+ content = []
+ valid_map = [
+ 'address',
+ 'netmask',
+ 'broadcast',
+ 'metric',
+ 'gateway',
+ 'pointopoint',
+ 'mtu',
+ 'scope',
+ 'dns_search',
+ 'dns_nameservers',
+ ]
+ for key, value in subnet.items():
+ if value and key in valid_map:
+ if type(value) == list:
+ value = " ".join(value)
+ if '_' in key:
+ key = key.replace('_', '-')
+ content.append(" {} {}".format(key, value))
+
+ return content
+
+
+# TODO: switch to valid_map for attrs
+
+def _iface_add_attrs(iface):
+ content = []
+ ignore_map = [
+ 'control',
+ 'index',
+ 'inet',
+ 'mode',
+ 'name',
+ 'subnets',
+ 'type',
+ ]
+ if iface['type'] not in ['bond', 'bridge', 'vlan']:
+ ignore_map.append('mac_address')
+
+ for key, value in iface.items():
+ if value and key not in ignore_map:
+ if type(value) == list:
+ value = " ".join(value)
+ content.append(" {} {}".format(key, value))
+
+ return content
+
+
+def _iface_start_entry(iface, index):
+ fullname = iface['name']
+ if index != 0:
+ fullname += ":%s" % index
+
+ control = iface['control']
+ if control == "auto":
+ cverb = "auto"
+ elif control in ("hotplug",):
+ cverb = "allow-" + control
+ else:
+ cverb = "# control-" + control
+
+ subst = iface.copy()
+ subst.update({'fullname': fullname, 'cverb': cverb})
+
+ return ["{cverb} {fullname}".format(**subst),
+ "iface {fullname} {inet} {mode}".format(**subst)]
+
+
+def _parse_deb_config_data(ifaces, contents, src_dir, src_path):
+ """Parses the file contents, placing result into ifaces.
+
+ '_source_path' is added to every dictionary entry to define which file
+ the configration information came from.
+
+ :param ifaces: interface dictionary
+ :param contents: contents of interfaces file
+ :param src_dir: directory interfaces file was located
+ :param src_path: file path the `contents` was read
+ """
+ currif = None
+ for line in contents.splitlines():
+ line = line.strip()
+ if line.startswith('#'):
+ continue
+ split = line.split(' ')
+ option = split[0]
+ if option == "source-directory":
+ parsed_src_dir = split[1]
+ if not parsed_src_dir.startswith("/"):
+ parsed_src_dir = os.path.join(src_dir, parsed_src_dir)
+ for expanded_path in glob.glob(parsed_src_dir):
+ dir_contents = os.listdir(expanded_path)
+ dir_contents = [
+ os.path.join(expanded_path, path)
+ for path in dir_contents
+ if (os.path.isfile(os.path.join(expanded_path, path)) and
+ re.match("^[a-zA-Z0-9_-]+$", path) is not None)
+ ]
+ for entry in dir_contents:
+ with open(entry, "r") as fp:
+ src_data = fp.read().strip()
+ abs_entry = os.path.abspath(entry)
+ _parse_deb_config_data(
+ ifaces, src_data,
+ os.path.dirname(abs_entry), abs_entry)
+ elif option == "source":
+ new_src_path = split[1]
+ if not new_src_path.startswith("/"):
+ new_src_path = os.path.join(src_dir, new_src_path)
+ for expanded_path in glob.glob(new_src_path):
+ with open(expanded_path, "r") as fp:
+ src_data = fp.read().strip()
+ abs_path = os.path.abspath(expanded_path)
+ _parse_deb_config_data(
+ ifaces, src_data,
+ os.path.dirname(abs_path), abs_path)
+ elif option == "auto":
+ for iface in split[1:]:
+ if iface not in ifaces:
+ ifaces[iface] = {
+ # Include the source path this interface was found in.
+ "_source_path": src_path
+ }
+ ifaces[iface]['auto'] = True
+ elif option == "iface":
+ iface, family, method = split[1:4]
+ if iface not in ifaces:
+ ifaces[iface] = {
+ # Include the source path this interface was found in.
+ "_source_path": src_path
+ }
+ elif 'family' in ifaces[iface]:
+ raise ParserError(
+ "Interface %s can only be defined once. "
+ "Re-defined in '%s'." % (iface, src_path))
+ ifaces[iface]['family'] = family
+ ifaces[iface]['method'] = method
+ currif = iface
+ elif option == "hwaddress":
+ if split[1] == "ether":
+ val = split[2]
+ else:
+ val = split[1]
+ ifaces[currif]['hwaddress'] = val
+ elif option in NET_CONFIG_OPTIONS:
+ ifaces[currif][option] = split[1]
+ elif option in NET_CONFIG_COMMANDS:
+ if option not in ifaces[currif]:
+ ifaces[currif][option] = []
+ ifaces[currif][option].append(' '.join(split[1:]))
+ elif option.startswith('dns-'):
+ if 'dns' not in ifaces[currif]:
+ ifaces[currif]['dns'] = {}
+ if option == 'dns-search':
+ ifaces[currif]['dns']['search'] = []
+ for domain in split[1:]:
+ ifaces[currif]['dns']['search'].append(domain)
+ elif option == 'dns-nameservers':
+ ifaces[currif]['dns']['nameservers'] = []
+ for server in split[1:]:
+ ifaces[currif]['dns']['nameservers'].append(server)
+ elif option.startswith('bridge_'):
+ if 'bridge' not in ifaces[currif]:
+ ifaces[currif]['bridge'] = {}
+ if option in NET_CONFIG_BRIDGE_OPTIONS:
+ bridge_option = option.replace('bridge_', '', 1)
+ ifaces[currif]['bridge'][bridge_option] = split[1]
+ elif option == "bridge_ports":
+ ifaces[currif]['bridge']['ports'] = []
+ for iface in split[1:]:
+ ifaces[currif]['bridge']['ports'].append(iface)
+ elif option == "bridge_hw" and split[1].lower() == "mac":
+ ifaces[currif]['bridge']['mac'] = split[2]
+ elif option == "bridge_pathcost":
+ if 'pathcost' not in ifaces[currif]['bridge']:
+ ifaces[currif]['bridge']['pathcost'] = {}
+ ifaces[currif]['bridge']['pathcost'][split[1]] = split[2]
+ elif option == "bridge_portprio":
+ if 'portprio' not in ifaces[currif]['bridge']:
+ ifaces[currif]['bridge']['portprio'] = {}
+ ifaces[currif]['bridge']['portprio'][split[1]] = split[2]
+ elif option.startswith('bond-'):
+ if 'bond' not in ifaces[currif]:
+ ifaces[currif]['bond'] = {}
+ bond_option = option.replace('bond-', '', 1)
+ ifaces[currif]['bond'][bond_option] = split[1]
+ for iface in ifaces.keys():
+ if 'auto' not in ifaces[iface]:
+ ifaces[iface]['auto'] = False
+
+
+def parse_deb_config(path):
+ """Parses a debian network configuration file."""
+ ifaces = {}
+ with open(path, "r") as fp:
+ contents = fp.read().strip()
+ abs_path = os.path.abspath(path)
+ _parse_deb_config_data(
+ ifaces, contents,
+ os.path.dirname(abs_path), abs_path)
+ return ifaces
+
+
+def convert_eni_data(eni_data):
+ # return a network config representation of what is in eni_data
+ ifaces = {}
+ _parse_deb_config_data(ifaces, eni_data, src_dir=None, src_path=None)
+ return _ifaces_to_net_config_data(ifaces)
+
+
+def _ifaces_to_net_config_data(ifaces):
+ """Return network config that represents the ifaces data provided.
+ ifaces = parse_deb_config("/etc/network/interfaces")
+ config = ifaces_to_net_config_data(ifaces)
+ state = parse_net_config_data(config)."""
+ devs = {}
+ for name, data in ifaces.items():
+ # devname is 'eth0' for name='eth0:1'
+ devname = name.partition(":")[0]
+ if devname == "lo":
+ # currently provding 'lo' in network config results in duplicate
+ # entries. in rendered interfaces file. so skip it.
+ continue
+ if devname not in devs:
+ devs[devname] = {'type': 'physical', 'name': devname,
+ 'subnets': []}
+ # this isnt strictly correct, but some might specify
+ # hwaddress on a nic for matching / declaring name.
+ if 'hwaddress' in data:
+ devs[devname]['mac_address'] = data['hwaddress']
+ subnet = {'_orig_eni_name': name, 'type': data['method']}
+ if data.get('auto'):
+ subnet['control'] = 'auto'
+ else:
+ subnet['control'] = 'manual'
+
+ if data.get('method') == 'static':
+ subnet['address'] = data['address']
+
+ for copy_key in ('netmask', 'gateway', 'broadcast'):
+ if copy_key in data:
+ subnet[copy_key] = data[copy_key]
+
+ if 'dns' in data:
+ for n in ('nameservers', 'search'):
+ if n in data['dns'] and data['dns'][n]:
+ subnet['dns_' + n] = data['dns'][n]
+ devs[devname]['subnets'].append(subnet)
+
+ return {'version': 1,
+ 'config': [devs[d] for d in sorted(devs)]}
+
+
+class Renderer(renderer.Renderer):
+ """Renders network information in a /etc/network/interfaces format."""
+
+ def __init__(self, config=None):
+ if not config:
+ config = {}
+ self.eni_path = config.get('eni_path', 'etc/network/interfaces')
+ self.links_path_prefix = config.get(
+ 'links_path_prefix', 'etc/systemd/network/50-cloud-init-')
+ self.netrules_path = config.get(
+ 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules')
+
+ def _render_route(self, route, indent=""):
+ """When rendering routes for an iface, in some cases applying a route
+ may result in the route command returning non-zero which produces
+ some confusing output for users manually using ifup/ifdown[1]. To
+ that end, we will optionally include an '|| true' postfix to each
+ route line allowing users to work with ifup/ifdown without using
+ --force option.
+
+ We may at somepoint not want to emit this additional postfix, and
+ add a 'strict' flag to this function. When called with strict=True,
+ then we will not append the postfix.
+
+ 1. http://askubuntu.com/questions/168033/
+ how-to-set-static-routes-in-ubuntu-server
+ """
+ content = []
+ up = indent + "post-up route add"
+ down = indent + "pre-down route del"
+ or_true = " || true"
+ mapping = {
+ 'network': '-net',
+ 'netmask': 'netmask',
+ 'gateway': 'gw',
+ 'metric': 'metric',
+ }
+ if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
+ default_gw = " default gw %s" % route['gateway']
+ content.append(up + default_gw + or_true)
+ content.append(down + default_gw + or_true)
+ elif route['network'] == '::' and route['netmask'] == 0:
+ # ipv6!
+ default_gw = " -A inet6 default gw %s" % route['gateway']
+ content.append(up + default_gw + or_true)
+ content.append(down + default_gw + or_true)
+ else:
+ route_line = ""
+ for k in ['network', 'netmask', 'gateway', 'metric']:
+ if k in route:
+ route_line += " %s %s" % (mapping[k], route[k])
+ content.append(up + route_line + or_true)
+ content.append(down + route_line + or_true)
+ return content
+
+ def _render_iface(self, iface):
+ lines = []
+ subnets = iface.get('subnets', {})
+ if subnets:
+ for index, subnet in zip(range(0, len(subnets)), subnets):
+ iface['index'] = index
+ iface['mode'] = subnet['type']
+ iface['control'] = subnet.get('control', 'auto')
+ if iface['mode'].endswith('6'):
+ iface['inet'] += '6'
+ elif iface['mode'] == 'static' and ":" in subnet['address']:
+ iface['inet'] += '6'
+ if iface['mode'].startswith('dhcp'):
+ iface['mode'] = 'dhcp'
+
+ lines.extend(_iface_start_entry(iface, index))
+ lines.extend(_iface_add_subnet(iface, subnet))
+ lines.extend(_iface_add_attrs(iface))
+ lines.append("")
+ else:
+ # ifenslave docs say to auto the slave devices
+ if 'bond-master' in iface:
+ lines.append("auto {name}".format(**iface))
+ lines.append("iface {name} {inet} {mode}".format(**iface))
+ lines.extend(_iface_add_attrs(iface))
+ return lines
+
+ def _render_interfaces(self, network_state):
+ '''Given state, emit etc/network/interfaces content.'''
+
+ content = ""
+
+ # handle 'lo' specifically as we need to insert the global dns entries
+ # there (as that is the only interface) that will be always up.
+ lo = {'name': 'lo', 'type': 'physical', 'inet': 'inet',
+ 'subnets': [{'type': 'loopback', 'control': 'auto'}]}
+ for iface in network_state.iter_interfaces():
+ if iface.get('name') == "lo":
+ lo = copy.deepcopy(iface)
+
+ nameservers = network_state.dns_nameservers
+ if nameservers:
+ lo['subnets'][0]["dns_nameservers"] = (" ".join(nameservers))
+
+ searchdomains = network_state.dns_searchdomains
+ if searchdomains:
+ lo['subnets'][0]["dns_search"] = (" ".join(searchdomains))
+
+ ''' Apply a sort order to ensure that we write out
+ the physical interfaces first; this is critical for
+ bonding
+ '''
+ order = {
+ 'physical': 0,
+ 'bond': 1,
+ 'bridge': 2,
+ 'vlan': 3,
+ }
+
+ sections = [self._render_iface(lo)]
+ for iface in sorted(network_state.iter_interfaces(),
+ key=lambda k: (order[k['type']], k['name'])):
+
+ if iface.get('name') == "lo":
+ continue
+ sections.append(self._render_iface(iface))
+
+ for route in network_state.iter_routes():
+ sections.append(self._render_route(route))
+
+ # global replacements until v2 format
+ content = ''.join(['\n'.join(s) + '\n\n' for s in sections])
+ return content
+
+ def render_network_state(self, target, network_state):
+ fpeni = os.path.join(target, self.eni_path)
+ util.ensure_dir(os.path.dirname(fpeni))
+ util.write_file(fpeni, self._render_interfaces(network_state))
+
+ if self.netrules_path:
+ netrules = os.path.join(target, self.netrules_path)
+ util.ensure_dir(os.path.dirname(netrules))
+ util.write_file(netrules,
+ self._render_persistent_net(network_state))
+
+ if self.links_path_prefix:
+ self._render_systemd_links(target, network_state,
+ links_prefix=self.links_path_prefix)
+
+ def _render_systemd_links(self, target, network_state, links_prefix):
+ fp_prefix = os.path.join(target, links_prefix)
+ for f in glob.glob(fp_prefix + "*"):
+ os.unlink(f)
+ for iface in network_state.iter_interfaces():
+ if (iface['type'] == 'physical' and 'name' in iface and
+ iface.get('mac_address')):
+ fname = fp_prefix + iface['name'] + ".link"
+ content = "\n".join([
+ "[Match]",
+ "MACAddress=" + iface['mac_address'],
+ "",
+ "[Link]",
+ "Name=" + iface['name'],
+ ""
+ ])
+ util.write_file(fname, content)
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
index 4c726ab4..8ca5106f 100644
--- a/cloudinit/net/network_state.py
+++ b/cloudinit/net/network_state.py
@@ -15,9 +15,13 @@
# You should have received a copy of the GNU Affero General Public License
# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
-from cloudinit import log as logging
+import copy
+import functools
+import logging
+
+import six
+
from cloudinit import util
-from cloudinit.util import yaml_dumps as dump_config
LOG = logging.getLogger(__name__)
@@ -27,80 +31,198 @@ NETWORK_STATE_REQUIRED_KEYS = {
}
+def parse_net_config_data(net_config, skip_broken=True):
+ """Parses the config, returns NetworkState object
+
+ :param net_config: curtin network config dict
+ """
+ state = None
+ if 'version' in net_config and 'config' in net_config:
+ nsi = NetworkStateInterpreter(version=net_config.get('version'),
+ config=net_config.get('config'))
+ nsi.parse_config(skip_broken=skip_broken)
+ state = nsi.network_state
+ return state
+
+
+def parse_net_config(path, skip_broken=True):
+ """Parses a curtin network configuration file and
+ return network state"""
+ ns = None
+ net_config = util.read_conf(path)
+ if 'network' in net_config:
+ ns = parse_net_config_data(net_config.get('network'),
+ skip_broken=skip_broken)
+ return ns
+
+
def from_state_file(state_file):
- network_state = None
state = util.read_conf(state_file)
- network_state = NetworkState()
- network_state.load(state)
+ nsi = NetworkStateInterpreter()
+ nsi.load(state)
+ return nsi
+
+
+def diff_keys(expected, actual):
+ missing = set(expected)
+ for key in actual:
+ missing.discard(key)
+ return missing
+
+
+class InvalidCommand(Exception):
+ pass
+
+
+def ensure_command_keys(required_keys):
+
+ def wrapper(func):
+
+ @functools.wraps(func)
+ def decorator(self, command, *args, **kwargs):
+ if required_keys:
+ missing_keys = diff_keys(required_keys, command)
+ if missing_keys:
+ raise InvalidCommand("Command missing %s of required"
+ " keys %s" % (missing_keys,
+ required_keys))
+ return func(self, command, *args, **kwargs)
+
+ return decorator
+
+ return wrapper
+
+
+class CommandHandlerMeta(type):
+ """Metaclass that dynamically creates a 'command_handlers' attribute.
- return network_state
+ This will scan the to-be-created class for methods that start with
+ 'handle_' and on finding those will populate a class attribute mapping
+ so that those methods can be quickly located and called.
+ """
+ def __new__(cls, name, parents, dct):
+ command_handlers = {}
+ for attr_name, attr in dct.items():
+ if callable(attr) and attr_name.startswith('handle_'):
+ handles_what = attr_name[len('handle_'):]
+ if handles_what:
+ command_handlers[handles_what] = attr
+ dct['command_handlers'] = command_handlers
+ return super(CommandHandlerMeta, cls).__new__(cls, name,
+ parents, dct)
class NetworkState(object):
- def __init__(self, version=NETWORK_STATE_VERSION, config=None):
- self.version = version
- self.config = config
- self.network_state = {
- 'interfaces': {},
- 'routes': [],
- 'dns': {
- 'nameservers': [],
- 'search': [],
- }
+
+ def __init__(self, network_state, version=NETWORK_STATE_VERSION):
+ self._network_state = copy.deepcopy(network_state)
+ self._version = version
+
+ @property
+ def version(self):
+ return self._version
+
+ def iter_routes(self, filter_func=None):
+ for route in self._network_state.get('routes', []):
+ if filter_func is not None:
+ if filter_func(route):
+ yield route
+ else:
+ yield route
+
+ @property
+ def dns_nameservers(self):
+ try:
+ return self._network_state['dns']['nameservers']
+ except KeyError:
+ return []
+
+ @property
+ def dns_searchdomains(self):
+ try:
+ return self._network_state['dns']['search']
+ except KeyError:
+ return []
+
+ def iter_interfaces(self, filter_func=None):
+ ifaces = self._network_state.get('interfaces', {})
+ for iface in six.itervalues(ifaces):
+ if filter_func is None:
+ yield iface
+ else:
+ if filter_func(iface):
+ yield iface
+
+
+@six.add_metaclass(CommandHandlerMeta)
+class NetworkStateInterpreter(object):
+
+ initial_network_state = {
+ 'interfaces': {},
+ 'routes': [],
+ 'dns': {
+ 'nameservers': [],
+ 'search': [],
}
- self.command_handlers = self.get_command_handlers()
+ }
- def get_command_handlers(self):
- METHOD_PREFIX = 'handle_'
- methods = filter(lambda x: callable(getattr(self, x)) and
- x.startswith(METHOD_PREFIX), dir(self))
- handlers = {}
- for m in methods:
- key = m.replace(METHOD_PREFIX, '')
- handlers[key] = getattr(self, m)
+ def __init__(self, version=NETWORK_STATE_VERSION, config=None):
+ self._version = version
+ self._config = config
+ self._network_state = copy.deepcopy(self.initial_network_state)
+ self._parsed = False
- return handlers
+ @property
+ def network_state(self):
+ return NetworkState(self._network_state, version=self._version)
def dump(self):
state = {
- 'version': self.version,
- 'config': self.config,
- 'network_state': self.network_state,
+ 'version': self._version,
+ 'config': self._config,
+ 'network_state': self._network_state,
}
- return dump_config(state)
+ return util.yaml_dumps(state)
def load(self, state):
if 'version' not in state:
LOG.error('Invalid state, missing version field')
- raise Exception('Invalid state, missing version field')
+ raise ValueError('Invalid state, missing version field')
required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']]
- if not self.valid_command(state, required_keys):
- msg = 'Invalid state, missing keys: {}'.format(required_keys)
+ missing_keys = diff_keys(required_keys, state)
+ if missing_keys:
+ msg = 'Invalid state, missing keys: %s' % (missing_keys)
LOG.error(msg)
- raise Exception(msg)
+ raise ValueError(msg)
# v1 - direct attr mapping, except version
for key in [k for k in required_keys if k not in ['version']]:
setattr(self, key, state[key])
- self.command_handlers = self.get_command_handlers()
def dump_network_state(self):
- return dump_config(self.network_state)
+ return util.yaml_dumps(self._network_state)
- def parse_config(self):
+ def parse_config(self, skip_broken=True):
# rebuild network state
- for command in self.config:
- handler = self.command_handlers.get(command['type'])
- handler(command)
-
- def valid_command(self, command, required_keys):
- if not required_keys:
- return False
-
- found_keys = [key for key in command.keys() if key in required_keys]
- return len(found_keys) == len(required_keys)
-
+ for command in self._config:
+ command_type = command['type']
+ try:
+ handler = self.command_handlers[command_type]
+ except KeyError:
+ raise RuntimeError("No handler found for"
+ " command '%s'" % command_type)
+ try:
+ handler(self, command)
+ except InvalidCommand:
+ if not skip_broken:
+ raise
+ else:
+ LOG.warn("Skipping invalid command: %s", command,
+ exc_info=True)
+ LOG.debug(self.dump_network_state())
+
+ @ensure_command_keys(['name'])
def handle_physical(self, command):
'''
command = {
@@ -112,15 +234,8 @@ class NetworkState(object):
]
}
'''
- required_keys = [
- 'name',
- ]
- if not self.valid_command(command, required_keys):
- LOG.warn('Skipping Invalid command: {}'.format(command))
- LOG.debug(self.dump_network_state())
- return
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces', {})
iface = interfaces.get(command['name'], {})
for param, val in command.get('params', {}).items():
iface.update({param: val})
@@ -146,9 +261,10 @@ class NetworkState(object):
'gateway': None,
'subnets': subnets,
})
- self.network_state['interfaces'].update({command.get('name'): iface})
+ self._network_state['interfaces'].update({command.get('name'): iface})
self.dump_network_state()
+ @ensure_command_keys(['name', 'vlan_id', 'vlan_link'])
def handle_vlan(self, command):
'''
auto eth0.222
@@ -158,23 +274,14 @@ class NetworkState(object):
hwaddress ether BC:76:4E:06:96:B3
vlan-raw-device eth0
'''
- required_keys = [
- 'name',
- 'vlan_link',
- 'vlan_id',
- ]
- if not self.valid_command(command, required_keys):
- print('Skipping Invalid command: {}'.format(command))
- print(self.dump_network_state())
- return
-
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces', {})
self.handle_physical(command)
iface = interfaces.get(command.get('name'), {})
iface['vlan-raw-device'] = command.get('vlan_link')
iface['vlan_id'] = command.get('vlan_id')
interfaces.update({iface['name']: iface})
+ @ensure_command_keys(['name', 'bond_interfaces', 'params'])
def handle_bond(self, command):
'''
#/etc/network/interfaces
@@ -200,23 +307,14 @@ class NetworkState(object):
bond-updelay 200
bond-lacp-rate 4
'''
- required_keys = [
- 'name',
- 'bond_interfaces',
- 'params',
- ]
- if not self.valid_command(command, required_keys):
- print('Skipping Invalid command: {}'.format(command))
- print(self.dump_network_state())
- return
self.handle_physical(command)
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces')
iface = interfaces.get(command.get('name'), {})
for param, val in command.get('params').items():
iface.update({param: val})
iface.update({'bond-slaves': 'none'})
- self.network_state['interfaces'].update({iface['name']: iface})
+ self._network_state['interfaces'].update({iface['name']: iface})
# handle bond slaves
for ifname in command.get('bond_interfaces'):
@@ -228,14 +326,15 @@ class NetworkState(object):
# inject placeholder
self.handle_physical(cmd)
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces', {})
bond_if = interfaces.get(ifname)
bond_if['bond-master'] = command.get('name')
# copy in bond config into slave
for param, val in command.get('params').items():
bond_if.update({param: val})
- self.network_state['interfaces'].update({ifname: bond_if})
+ self._network_state['interfaces'].update({ifname: bond_if})
+ @ensure_command_keys(['name', 'bridge_interfaces', 'params'])
def handle_bridge(self, command):
'''
auto br0
@@ -263,19 +362,10 @@ class NetworkState(object):
"bridge_waitport",
]
'''
- required_keys = [
- 'name',
- 'bridge_interfaces',
- 'params',
- ]
- if not self.valid_command(command, required_keys):
- print('Skipping Invalid command: {}'.format(command))
- print(self.dump_network_state())
- return
# find one of the bridge port ifaces to get mac_addr
# handle bridge_slaves
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces', {})
for ifname in command.get('bridge_interfaces'):
if ifname in interfaces:
continue
@@ -286,7 +376,7 @@ class NetworkState(object):
# inject placeholder
self.handle_physical(cmd)
- interfaces = self.network_state.get('interfaces')
+ interfaces = self._network_state.get('interfaces', {})
self.handle_physical(command)
iface = interfaces.get(command.get('name'), {})
iface['bridge_ports'] = command['bridge_interfaces']
@@ -295,16 +385,9 @@ class NetworkState(object):
interfaces.update({iface['name']: iface})
+ @ensure_command_keys(['address'])
def handle_nameserver(self, command):
- required_keys = [
- 'address',
- ]
- if not self.valid_command(command, required_keys):
- print('Skipping Invalid command: {}'.format(command))
- print(self.dump_network_state())
- return
-
- dns = self.network_state.get('dns')
+ dns = self._network_state.get('dns')
if 'address' in command:
addrs = command['address']
if not type(addrs) == list:
@@ -318,16 +401,9 @@ class NetworkState(object):
for path in paths:
dns['search'].append(path)
+ @ensure_command_keys(['destination'])
def handle_route(self, command):
- required_keys = [
- 'destination',
- ]
- if not self.valid_command(command, required_keys):
- print('Skipping Invalid command: {}'.format(command))
- print(self.dump_network_state())
- return
-
- routes = self.network_state.get('routes')
+ routes = self._network_state.get('routes', [])
network, cidr = command['destination'].split("/")
netmask = cidr2mask(int(cidr))
route = {
@@ -376,72 +452,3 @@ def mask2cidr(mask):
return ipv4mask2cidr(mask)
else:
return mask
-
-
-if __name__ == '__main__':
- import random
- import sys
-
- from cloudinit import net
-
- def load_config(nc):
- version = nc.get('version')
- config = nc.get('config')
- return (version, config)
-
- def test_parse(network_config):
- (version, config) = load_config(network_config)
- ns1 = NetworkState(version=version, config=config)
- ns1.parse_config()
- random.shuffle(config)
- ns2 = NetworkState(version=version, config=config)
- ns2.parse_config()
- print("----NS1-----")
- print(ns1.dump_network_state())
- print()
- print("----NS2-----")
- print(ns2.dump_network_state())
- print("NS1 == NS2 ?=> {}".format(
- ns1.network_state == ns2.network_state))
- eni = net.render_interfaces(ns2.network_state)
- print(eni)
- udev_rules = net.render_persistent_net(ns2.network_state)
- print(udev_rules)
-
- def test_dump_and_load(network_config):
- print("Loading network_config into NetworkState")
- (version, config) = load_config(network_config)
- ns1 = NetworkState(version=version, config=config)
- ns1.parse_config()
- print("Dumping state to file")
- ns1_dump = ns1.dump()
- ns1_state = "/tmp/ns1.state"
- with open(ns1_state, "w+") as f:
- f.write(ns1_dump)
-
- print("Loading state from file")
- ns2 = from_state_file(ns1_state)
- print("NS1 == NS2 ?=> {}".format(
- ns1.network_state == ns2.network_state))
-
- def test_output(network_config):
- (version, config) = load_config(network_config)
- ns1 = NetworkState(version=version, config=config)
- ns1.parse_config()
- random.shuffle(config)
- ns2 = NetworkState(version=version, config=config)
- ns2.parse_config()
- print("NS1 == NS2 ?=> {}".format(
- ns1.network_state == ns2.network_state))
- eni_1 = net.render_interfaces(ns1.network_state)
- eni_2 = net.render_interfaces(ns2.network_state)
- print(eni_1)
- print(eni_2)
- print("eni_1 == eni_2 ?=> {}".format(
- eni_1 == eni_2))
-
- y = util.read_conf(sys.argv[1])
- network_config = y.get('network')
- test_parse(network_config)
- test_dump_and_load(network_config)
- test_output(network_config)
diff --git a/cloudinit/net/renderer.py b/cloudinit/net/renderer.py
new file mode 100644
index 00000000..310cbe0d
--- /dev/null
+++ b/cloudinit/net/renderer.py
@@ -0,0 +1,48 @@
+# Copyright (C) 2013-2014 Canonical Ltd.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+# Author: Blake Rouse <blake.rouse@canonical.com>
+#
+# Curtin is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# Curtin 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 Affero General Public License for
+# more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
+
+import six
+
+from .udev import generate_udev_rule
+
+
+def filter_by_type(match_type):
+ return lambda iface: match_type == iface['type']
+
+
+def filter_by_name(match_name):
+ return lambda iface: match_name == iface['name']
+
+
+filter_by_physical = filter_by_type('physical')
+
+
+class Renderer(object):
+
+ @staticmethod
+ def _render_persistent_net(network_state):
+ """Given state, emit udev rules to map mac to ifname."""
+ # TODO(harlowja): this seems shared between eni renderer and
+ # this, so move it to a shared location.
+ content = six.StringIO()
+ for iface in network_state.iter_interfaces(filter_by_physical):
+ # for physical interfaces write out a persist net udev rule
+ if 'name' in iface and iface.get('mac_address'):
+ content.write(generate_udev_rule(iface['name'],
+ iface['mac_address']))
+ return content.getvalue()
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
new file mode 100644
index 00000000..c53acf71
--- /dev/null
+++ b/cloudinit/net/sysconfig.py
@@ -0,0 +1,400 @@
+# vi: ts=4 expandtab
+#
+# 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 os
+import re
+
+import six
+
+from cloudinit.distros.parsers import resolv_conf
+from cloudinit import util
+
+from . import renderer
+
+
+def _make_header(sep='#'):
+ lines = [
+ "Created by cloud-init on instance boot automatically, do not edit.",
+ "",
+ ]
+ for i in range(0, len(lines)):
+ if lines[i]:
+ lines[i] = sep + " " + lines[i]
+ else:
+ lines[i] = sep
+ return "\n".join(lines)
+
+
+def _is_default_route(route):
+ if route['network'] == '::' and route['netmask'] == 0:
+ return True
+ if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
+ return True
+ return False
+
+
+def _quote_value(value):
+ if re.search(r"\s", value):
+ # This doesn't handle complex cases...
+ if value.startswith('"') and value.endswith('"'):
+ return value
+ else:
+ return '"%s"' % value
+ else:
+ return value
+
+
+class ConfigMap(object):
+ """Sysconfig like dictionary object."""
+
+ # Why does redhat prefer yes/no to true/false??
+ _bool_map = {
+ True: 'yes',
+ False: 'no',
+ }
+
+ def __init__(self):
+ self._conf = {}
+
+ def __setitem__(self, key, value):
+ self._conf[key] = value
+
+ def drop(self, key):
+ self._conf.pop(key, None)
+
+ def __len__(self):
+ return len(self._conf)
+
+ def to_string(self):
+ buf = six.StringIO()
+ buf.write(_make_header())
+ if self._conf:
+ buf.write("\n")
+ for key in sorted(self._conf.keys()):
+ value = self._conf[key]
+ if isinstance(value, bool):
+ value = self._bool_map[value]
+ if not isinstance(value, six.string_types):
+ value = str(value)
+ buf.write("%s=%s\n" % (key, _quote_value(value)))
+ return buf.getvalue()
+
+
+class Route(ConfigMap):
+ """Represents a route configuration."""
+
+ route_fn_tpl = '%(base)s/network-scripts/route-%(name)s'
+
+ def __init__(self, route_name, base_sysconf_dir):
+ super(Route, self).__init__()
+ self.last_idx = 1
+ self.has_set_default = False
+ self._route_name = route_name
+ self._base_sysconf_dir = base_sysconf_dir
+
+ def copy(self):
+ r = Route(self._route_name, self._base_sysconf_dir)
+ r._conf = self._conf.copy()
+ r.last_idx = self.last_idx
+ r.has_set_default = self.has_set_default
+ return r
+
+ @property
+ def path(self):
+ return self.route_fn_tpl % ({'base': self._base_sysconf_dir,
+ 'name': self._route_name})
+
+
+class NetInterface(ConfigMap):
+ """Represents a sysconfig/networking-script (and its config + children)."""
+
+ iface_fn_tpl = '%(base)s/network-scripts/ifcfg-%(name)s'
+
+ iface_types = {
+ 'ethernet': 'Ethernet',
+ 'bond': 'Bond',
+ 'bridge': 'Bridge',
+ }
+
+ def __init__(self, iface_name, base_sysconf_dir, kind='ethernet'):
+ super(NetInterface, self).__init__()
+ self.children = []
+ self.routes = Route(iface_name, base_sysconf_dir)
+ self._kind = kind
+ self._iface_name = iface_name
+ self._conf['DEVICE'] = iface_name
+ self._conf['TYPE'] = self.iface_types[kind]
+ self._base_sysconf_dir = base_sysconf_dir
+
+ @property
+ def name(self):
+ return self._iface_name
+
+ @name.setter
+ def name(self, iface_name):
+ self._iface_name = iface_name
+ self._conf['DEVICE'] = iface_name
+
+ @property
+ def kind(self):
+ return self._kind
+
+ @kind.setter
+ def kind(self, kind):
+ self._kind = kind
+ self._conf['TYPE'] = self.iface_types[kind]
+
+ @property
+ def path(self):
+ return self.iface_fn_tpl % ({'base': self._base_sysconf_dir,
+ 'name': self.name})
+
+ def copy(self, copy_children=False, copy_routes=False):
+ c = NetInterface(self.name, self._base_sysconf_dir, kind=self._kind)
+ c._conf = self._conf.copy()
+ if copy_children:
+ c.children = list(self.children)
+ if copy_routes:
+ c.routes = self.routes.copy()
+ return c
+
+
+class Renderer(renderer.Renderer):
+ """Renders network information in a /etc/sysconfig format."""
+
+ # See: https://access.redhat.com/documentation/en-US/\
+ # Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/\
+ # s1-networkscripts-interfaces.html (or other docs for
+ # details about this)
+
+ iface_defaults = tuple([
+ ('ONBOOT', True),
+ ('USERCTL', False),
+ ('NM_CONTROLLED', False),
+ ('BOOTPROTO', 'none'),
+ ])
+
+ # If these keys exist, then there values will be used to form
+ # a BONDING_OPTS grouping; otherwise no grouping will be set.
+ bond_tpl_opts = tuple([
+ ('bond_mode', "mode=%s"),
+ ('bond_xmit_hash_policy', "xmit_hash_policy=%s"),
+ ('bond_miimon', "miimon=%s"),
+ ])
+
+ bridge_opts_keys = tuple([
+ ('bridge_stp', 'STP'),
+ ('bridge_ageing', 'AGEING'),
+ ('bridge_bridgeprio', 'PRIO'),
+ ])
+
+ def __init__(self, config=None):
+ if not config:
+ config = {}
+ self.sysconf_dir = config.get('sysconf_dir', 'etc/sysconfig/')
+ self.netrules_path = config.get(
+ 'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules')
+ self.dns_path = config.get('dns_path', 'etc/resolv.conf')
+
+ @classmethod
+ def _render_iface_shared(cls, iface, iface_cfg):
+ for k, v in cls.iface_defaults:
+ iface_cfg[k] = v
+ for (old_key, new_key) in [('mac_address', 'HWADDR'), ('mtu', 'MTU')]:
+ old_value = iface.get(old_key)
+ if old_value is not None:
+ iface_cfg[new_key] = old_value
+
+ @classmethod
+ def _render_subnet(cls, iface_cfg, route_cfg, subnet):
+ subnet_type = subnet.get('type')
+ if subnet_type == 'dhcp6':
+ iface_cfg['DHCPV6C'] = True
+ iface_cfg['IPV6INIT'] = True
+ iface_cfg['BOOTPROTO'] = 'dhcp'
+ elif subnet_type in ['dhcp4', 'dhcp']:
+ iface_cfg['BOOTPROTO'] = 'dhcp'
+ elif subnet_type == 'static':
+ iface_cfg['BOOTPROTO'] = 'static'
+ if subnet.get('ipv6'):
+ iface_cfg['IPV6ADDR'] = subnet['address']
+ iface_cfg['IPV6INIT'] = True
+ else:
+ iface_cfg['IPADDR'] = subnet['address']
+ else:
+ raise ValueError("Unknown subnet type '%s' found"
+ " for interface '%s'" % (subnet_type,
+ iface_cfg.name))
+ if 'netmask' in subnet:
+ iface_cfg['NETMASK'] = subnet['netmask']
+ for route in subnet.get('routes', []):
+ if _is_default_route(route):
+ if route_cfg.has_set_default:
+ raise ValueError("Duplicate declaration of default"
+ " route found for interface '%s'"
+ % (iface_cfg.name))
+ # NOTE(harlowja): ipv6 and ipv4 default gateways
+ gw_key = 'GATEWAY0'
+ nm_key = 'NETMASK0'
+ addr_key = 'ADDRESS0'
+ # The owning interface provides the default route.
+ #
+ # TODO(harlowja): add validation that no other iface has
+ # also provided the default route?
+ iface_cfg['DEFROUTE'] = True
+ if 'gateway' in route:
+ iface_cfg['GATEWAY'] = route['gateway']
+ route_cfg.has_set_default = True
+ else:
+ gw_key = 'GATEWAY%s' % route_cfg.last_idx
+ nm_key = 'NETMASK%s' % route_cfg.last_idx
+ addr_key = 'ADDRESS%s' % route_cfg.last_idx
+ route_cfg.last_idx += 1
+ for (old_key, new_key) in [('gateway', gw_key),
+ ('netmask', nm_key),
+ ('network', addr_key)]:
+ if old_key in route:
+ route_cfg[new_key] = route[old_key]
+
+ @classmethod
+ def _render_bonding_opts(cls, iface_cfg, iface):
+ bond_opts = []
+ for (bond_key, value_tpl) in cls.bond_tpl_opts:
+ # Seems like either dash or underscore is possible?
+ bond_keys = [bond_key, bond_key.replace("_", "-")]
+ for bond_key in bond_keys:
+ if bond_key in iface:
+ bond_value = iface[bond_key]
+ if isinstance(bond_value, (tuple, list)):
+ bond_value = " ".join(bond_value)
+ bond_opts.append(value_tpl % (bond_value))
+ break
+ if bond_opts:
+ iface_cfg['BONDING_OPTS'] = " ".join(bond_opts)
+
+ @classmethod
+ def _render_physical_interfaces(cls, network_state, iface_contents):
+ physical_filter = renderer.filter_by_physical
+ for iface in network_state.iter_interfaces(physical_filter):
+ iface_name = iface['name']
+ iface_subnets = iface.get("subnets", [])
+ iface_cfg = iface_contents[iface_name]
+ route_cfg = iface_cfg.routes
+ if len(iface_subnets) == 1:
+ cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])
+ elif len(iface_subnets) > 1:
+ for i, iface_subnet in enumerate(iface_subnets,
+ start=len(iface.children)):
+ iface_sub_cfg = iface_cfg.copy()
+ iface_sub_cfg.name = "%s:%s" % (iface_name, i)
+ iface.children.append(iface_sub_cfg)
+ cls._render_subnet(iface_sub_cfg, route_cfg, iface_subnet)
+
+ @classmethod
+ def _render_bond_interfaces(cls, network_state, iface_contents):
+ bond_filter = renderer.filter_by_type('bond')
+ for iface in network_state.iter_interfaces(bond_filter):
+ iface_name = iface['name']
+ iface_cfg = iface_contents[iface_name]
+ cls._render_bonding_opts(iface_cfg, iface)
+ iface_master_name = iface['bond-master']
+ iface_cfg['MASTER'] = iface_master_name
+ iface_cfg['SLAVE'] = True
+ # Ensure that the master interface (and any of its children)
+ # are actually marked as being bond types...
+ master_cfg = iface_contents[iface_master_name]
+ master_cfgs = [master_cfg]
+ master_cfgs.extend(master_cfg.children)
+ for master_cfg in master_cfgs:
+ master_cfg['BONDING_MASTER'] = True
+ master_cfg.kind = 'bond'
+
+ @staticmethod
+ def _render_vlan_interfaces(network_state, iface_contents):
+ vlan_filter = renderer.filter_by_type('vlan')
+ for iface in network_state.iter_interfaces(vlan_filter):
+ iface_name = iface['name']
+ iface_cfg = iface_contents[iface_name]
+ iface_cfg['VLAN'] = True
+ iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')]
+
+ @staticmethod
+ def _render_dns(network_state, existing_dns_path=None):
+ content = resolv_conf.ResolvConf("")
+ if existing_dns_path and os.path.isfile(existing_dns_path):
+ content = resolv_conf.ResolvConf(util.load_file(existing_dns_path))
+ for nameserver in network_state.dns_nameservers:
+ content.add_nameserver(nameserver)
+ for searchdomain in network_state.dns_searchdomains:
+ content.add_search_domain(searchdomain)
+ return "\n".join([_make_header(';'), str(content)])
+
+ @classmethod
+ def _render_bridge_interfaces(cls, network_state, iface_contents):
+ bridge_filter = renderer.filter_by_type('bridge')
+ for iface in network_state.iter_interfaces(bridge_filter):
+ iface_name = iface['name']
+ iface_cfg = iface_contents[iface_name]
+ iface_cfg.kind = 'bridge'
+ for old_key, new_key in cls.bridge_opts_keys:
+ if old_key in iface:
+ iface_cfg[new_key] = iface[old_key]
+ # Is this the right key to get all the connected interfaces?
+ for bridged_iface_name in iface.get('bridge_ports', []):
+ # Ensure all bridged interfaces are correctly tagged
+ # as being bridged to this interface.
+ bridged_cfg = iface_contents[bridged_iface_name]
+ bridged_cfgs = [bridged_cfg]
+ bridged_cfgs.extend(bridged_cfg.children)
+ for bridge_cfg in bridged_cfgs:
+ bridge_cfg['BRIDGE'] = iface_name
+
+ @classmethod
+ def _render_sysconfig(cls, base_sysconf_dir, network_state):
+ '''Given state, return /etc/sysconfig files + contents'''
+ iface_contents = {}
+ for iface in network_state.iter_interfaces():
+ iface_name = iface['name']
+ iface_cfg = NetInterface(iface_name, base_sysconf_dir)
+ cls._render_iface_shared(iface, iface_cfg)
+ iface_contents[iface_name] = iface_cfg
+ cls._render_physical_interfaces(network_state, iface_contents)
+ cls._render_bond_interfaces(network_state, iface_contents)
+ cls._render_vlan_interfaces(network_state, iface_contents)
+ cls._render_bridge_interfaces(network_state, iface_contents)
+ contents = {}
+ for iface_name, iface_cfg in iface_contents.items():
+ if iface_cfg or iface_cfg.children:
+ contents[iface_cfg.path] = iface_cfg.to_string()
+ for iface_cfg in iface_cfg.children:
+ if iface_cfg:
+ contents[iface_cfg.path] = iface_cfg.to_string()
+ if iface_cfg.routes:
+ contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()
+ return contents
+
+ def render_network_state(self, target, network_state):
+ base_sysconf_dir = os.path.join(target, self.sysconf_dir)
+ for path, data in self._render_sysconfig(base_sysconf_dir,
+ network_state).items():
+ util.write_file(path, data)
+ if self.dns_path:
+ dns_path = os.path.join(target, self.dns_path)
+ resolv_content = self._render_dns(network_state,
+ existing_dns_path=dns_path)
+ util.write_file(dns_path, resolv_content)
+ if self.netrules_path:
+ netrules_content = self._render_persistent_net(network_state)
+ netrules_path = os.path.join(target, self.netrules_path)
+ util.write_file(netrules_path, netrules_content)
diff --git a/cloudinit/serial.py b/cloudinit/serial.py
new file mode 100644
index 00000000..af45c13e
--- /dev/null
+++ b/cloudinit/serial.py
@@ -0,0 +1,50 @@
+# vi: ts=4 expandtab
+#
+# 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/>.
+
+
+from __future__ import absolute_import
+
+try:
+ from serial import Serial
+except ImportError:
+ # For older versions of python (ie 2.6) pyserial may not exist and/or
+ # work and/or be installed, so make a dummy/fake serial that blows up
+ # when used...
+ class Serial(object):
+ def __init__(self, *args, **kwargs):
+ pass
+
+ @staticmethod
+ def isOpen():
+ return False
+
+ @staticmethod
+ def write(data):
+ raise IOError("Unable to perform serial `write` operation,"
+ " pyserial not installed.")
+
+ @staticmethod
+ def readline():
+ raise IOError("Unable to perform serial `readline` operation,"
+ " pyserial not installed.")
+
+ @staticmethod
+ def flush():
+ raise IOError("Unable to perform serial `flush` operation,"
+ " pyserial not installed.")
+
+ @staticmethod
+ def read(size=1):
+ raise IOError("Unable to perform serial `read` operation,"
+ " pyserial not installed.")
diff --git a/cloudinit/sources/DataSourceAltCloud.py b/cloudinit/sources/DataSourceAltCloud.py
index cd61df31..a3529609 100644
--- a/cloudinit/sources/DataSourceAltCloud.py
+++ b/cloudinit/sources/DataSourceAltCloud.py
@@ -205,8 +205,7 @@ class DataSourceAltCloud(sources.DataSource):
_err.message)
return False
except OSError as _err:
- util.logexc(LOG, 'Failed command: %s\n%s', ' '.join(cmd),
- _err.message)
+ util.logexc(LOG, 'Failed command: %s\n%s', ' '.join(cmd), _err)
return False
floppy_dev = '/dev/fd0'
diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py
index 2d046600..8c7e8673 100644
--- a/cloudinit/sources/DataSourceAzure.py
+++ b/cloudinit/sources/DataSourceAzure.py
@@ -423,7 +423,7 @@ def write_files(datadir, files, dirmode=None):
elem.text = DEF_PASSWD_REDACTION
return ET.tostring(root)
except Exception:
- LOG.critical("failed to redact userpassword in {}".format(fname))
+ LOG.critical("failed to redact userpassword in %s", fname)
return cnt
if not datadir:
diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py
index 3cc9155d..3130e618 100644
--- a/cloudinit/sources/DataSourceConfigDrive.py
+++ b/cloudinit/sources/DataSourceConfigDrive.py
@@ -18,14 +18,14 @@
# 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 copy
import os
from cloudinit import log as logging
-from cloudinit import net
from cloudinit import sources
from cloudinit import util
+from cloudinit.net import eni
+
from cloudinit.sources.helpers import openstack
LOG = logging.getLogger(__name__)
@@ -53,6 +53,7 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource):
self._network_config = None
self.network_json = None
self.network_eni = None
+ self.known_macs = None
self.files = {}
def __str__(self):
@@ -147,9 +148,10 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource):
if self._network_config is None:
if self.network_json is not None:
LOG.debug("network config provided via network_json")
- self._network_config = convert_network_data(self.network_json)
+ self._network_config = openstack.convert_net_json(
+ self.network_json, known_macs=self.known_macs)
elif self.network_eni is not None:
- self._network_config = net.convert_eni_data(self.network_eni)
+ self._network_config = eni.convert_eni_data(self.network_eni)
LOG.debug("network config provided via converted eni data")
else:
LOG.debug("no network configuration available")
@@ -254,152 +256,12 @@ def find_candidate_devs(probe_optical=True):
return devices
-# Convert OpenStack ConfigDrive NetworkData json to network_config yaml
-def convert_network_data(network_json=None, known_macs=None):
- """Return a dictionary of network_config by parsing provided
- OpenStack ConfigDrive NetworkData json format
-
- OpenStack network_data.json provides a 3 element dictionary
- - "links" (links are network devices, physical or virtual)
- - "networks" (networks are ip network configurations for one or more
- links)
- - services (non-ip services, like dns)
-
- networks and links are combined via network items referencing specific
- links via a 'link_id' which maps to a links 'id' field.
-
- To convert this format to network_config yaml, we first iterate over the
- links and then walk the network list to determine if any of the networks
- utilize the current link; if so we generate a subnet entry for the device
-
- We also need to map network_data.json fields to network_config fields. For
- example, the network_data links 'id' field is equivalent to network_config
- 'name' field for devices. We apply more of this mapping to the various
- link types that we encounter.
-
- There are additional fields that are populated in the network_data.json
- from OpenStack that are not relevant to network_config yaml, so we
- enumerate a dictionary of valid keys for network_yaml and apply filtering
- to drop these superflous keys from the network_config yaml.
- """
- if network_json is None:
- return None
-
- # dict of network_config key for filtering network_json
- valid_keys = {
- 'physical': [
- 'name',
- 'type',
- 'mac_address',
- 'subnets',
- 'params',
- ],
- 'subnet': [
- 'type',
- 'address',
- 'netmask',
- 'broadcast',
- 'metric',
- 'gateway',
- 'pointopoint',
- 'mtu',
- 'scope',
- 'dns_nameservers',
- 'dns_search',
- 'routes',
- ],
- }
-
- links = network_json.get('links', [])
- networks = network_json.get('networks', [])
- services = network_json.get('services', [])
-
- config = []
- for link in links:
- subnets = []
- cfg = {k: v for k, v in link.items()
- if k in valid_keys['physical']}
- # 'name' is not in openstack spec yet, but we will support it if it is
- # present. The 'id' in the spec is currently implemented as the host
- # nic's name, meaning something like 'tap-adfasdffd'. We do not want
- # to name guest devices with such ugly names.
- if 'name' in link:
- cfg['name'] = link['name']
-
- for network in [n for n in networks
- if n['link'] == link['id']]:
- subnet = {k: v for k, v in network.items()
- if k in valid_keys['subnet']}
- if 'dhcp' in network['type']:
- t = 'dhcp6' if network['type'].startswith('ipv6') else 'dhcp4'
- subnet.update({
- 'type': t,
- })
- else:
- subnet.update({
- 'type': 'static',
- 'address': network.get('ip_address'),
- })
- subnets.append(subnet)
- cfg.update({'subnets': subnets})
- if link['type'] in ['ethernet', 'vif', 'ovs', 'phy']:
- cfg.update({
- 'type': 'physical',
- 'mac_address': link['ethernet_mac_address']})
- elif link['type'] in ['bond']:
- params = {}
- for k, v in link.items():
- if k == 'bond_links':
- continue
- elif k.startswith('bond'):
- params.update({k: v})
- cfg.update({
- 'bond_interfaces': copy.deepcopy(link['bond_links']),
- 'params': params,
- })
- elif link['type'] in ['vlan']:
- cfg.update({
- 'name': "%s.%s" % (link['vlan_link'],
- link['vlan_id']),
- 'vlan_link': link['vlan_link'],
- 'vlan_id': link['vlan_id'],
- 'mac_address': link['vlan_mac_address'],
- })
- else:
- raise ValueError(
- 'Unknown network_data link type: %s' % link['type'])
-
- config.append(cfg)
-
- need_names = [d for d in config
- if d.get('type') == 'physical' and 'name' not in d]
-
- if need_names:
- if known_macs is None:
- known_macs = net.get_interfaces_by_mac()
-
- for d in need_names:
- mac = d.get('mac_address')
- if not mac:
- raise ValueError("No mac_address or name entry for %s" % d)
- if mac not in known_macs:
- raise ValueError("Unable to find a system nic for %s" % d)
- d['name'] = known_macs[mac]
-
- for service in services:
- cfg = service
- cfg.update({'type': 'nameserver'})
- config.append(cfg)
-
- return {'version': 1, 'config': config}
-
-
# Legacy: Must be present in case we load an old pkl object
DataSourceConfigDriveNet = DataSourceConfigDrive
# Used to match classes to dependencies
datasources = [
- (DataSourceConfigDrive, (sources.DEP_FILESYSTEM, )),
+ (DataSourceConfigDrive, (sources.DEP_FILESYSTEM,)),
]
diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py
index 9234d1f8..c660a350 100644
--- a/cloudinit/sources/DataSourceGCE.py
+++ b/cloudinit/sources/DataSourceGCE.py
@@ -25,7 +25,7 @@ from cloudinit import util
LOG = logging.getLogger(__name__)
BUILTIN_DS_CONFIG = {
- 'metadata_url': 'http://metadata.google.internal./computeMetadata/v1/'
+ 'metadata_url': 'http://metadata.google.internal/computeMetadata/v1/'
}
REQUIRED_FIELDS = ('instance-id', 'availability-zone', 'local-hostname')
diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py
index 7e30118c..cdc9eef5 100644
--- a/cloudinit/sources/DataSourceNoCloud.py
+++ b/cloudinit/sources/DataSourceNoCloud.py
@@ -24,7 +24,7 @@ import errno
import os
from cloudinit import log as logging
-from cloudinit import net
+from cloudinit.net import eni
from cloudinit import sources
from cloudinit import util
@@ -34,7 +34,6 @@ LOG = logging.getLogger(__name__)
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.seed_dirs = [os.path.join(paths.seed_dir, 'nocloud'),
os.path.join(paths.seed_dir, 'nocloud-net')]
@@ -194,8 +193,7 @@ class DataSourceNoCloud(sources.DataSource):
# LP: #1568150 need getattr in the case that an old class object
# has been loaded from a pickled file and now executing new source.
dirs = getattr(self, 'seed_dirs', [self.seed_dir])
- quick_id = _quick_read_instance_id(cmdline_id=self.cmdline_id,
- dirs=dirs)
+ quick_id = _quick_read_instance_id(dirs=dirs)
if not quick_id:
return None
return quick_id == current
@@ -203,20 +201,19 @@ class DataSourceNoCloud(sources.DataSource):
@property
def network_config(self):
if self._network_config is None:
- if self.network_eni is not None:
- self._network_config = net.convert_eni_data(self.network_eni)
+ if self._network_eni is not None:
+ self._network_config = eni.convert_eni_data(self._network_eni)
return self._network_config
-def _quick_read_instance_id(cmdline_id, dirs=None):
+def _quick_read_instance_id(dirs=None):
if dirs is None:
dirs = []
iid_key = 'instance-id'
- if cmdline_id is None:
- fill = {}
- if parse_cmdline_data(cmdline_id, fill) and iid_key in fill:
- return fill[iid_key]
+ fill = {}
+ if load_cmdline_data(fill) and iid_key in fill:
+ return fill[iid_key]
for d in dirs:
if d is None:
diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py
index f2bb9366..43347cfb 100644
--- a/cloudinit/sources/DataSourceOVF.py
+++ b/cloudinit/sources/DataSourceOVF.py
@@ -37,16 +37,16 @@ from cloudinit.sources.helpers.vmware.imc.config_file \
import ConfigFile
from cloudinit.sources.helpers.vmware.imc.config_nic \
import NicConfigurator
+from cloudinit.sources.helpers.vmware.imc.guestcust_error \
+ import GuestCustErrorEnum
from cloudinit.sources.helpers.vmware.imc.guestcust_event \
import GuestCustEventEnum
from cloudinit.sources.helpers.vmware.imc.guestcust_state \
import GuestCustStateEnum
-from cloudinit.sourceshelpers.vmware.imc.guestcust_error \
- import GuestCustErrorEnum
-from cloudinit.sourceshelpers.vmware.imc.guestcust_util import (
- set_customization_status,
+from cloudinit.sources.helpers.vmware.imc.guestcust_util import (
+ enable_nics,
get_nics_to_enable,
- enable_nics
+ set_customization_status
)
LOG = logging.getLogger(__name__)
diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py
index 8f85b115..7b3a76b9 100644
--- a/cloudinit/sources/DataSourceOpenNebula.py
+++ b/cloudinit/sources/DataSourceOpenNebula.py
@@ -104,7 +104,7 @@ class DataSourceOpenNebula(sources.DataSource):
def get_hostname(self, fqdn=False, resolve_ip=None):
if resolve_ip is None:
- if self.dsmode == sources.DSMODE_NET:
+ if self.dsmode == sources.DSMODE_NETWORK:
resolve_ip = True
else:
resolve_ip = False
diff --git a/cloudinit/sources/DataSourceSmartOS.py b/cloudinit/sources/DataSourceSmartOS.py
index 0e03b04f..08bc132b 100644
--- a/cloudinit/sources/DataSourceSmartOS.py
+++ b/cloudinit/sources/DataSourceSmartOS.py
@@ -40,13 +40,11 @@ import random
import re
import socket
-import serial
-
from cloudinit import log as logging
+from cloudinit import serial
from cloudinit import sources
from cloudinit import util
-
LOG = logging.getLogger(__name__)
SMARTOS_ATTRIB_MAP = {
diff --git a/cloudinit/sources/helpers/openstack.py b/cloudinit/sources/helpers/openstack.py
index 3ccb11d3..d52cb56a 100644
--- a/cloudinit/sources/helpers/openstack.py
+++ b/cloudinit/sources/helpers/openstack.py
@@ -28,6 +28,7 @@ import six
from cloudinit import ec2_utils
from cloudinit import log as logging
+from cloudinit import net
from cloudinit import sources
from cloudinit import url_helper
from cloudinit import util
@@ -156,7 +157,7 @@ class BaseReader(object):
pass
@abc.abstractmethod
- def _path_read(self, path):
+ def _path_read(self, path, decode=False):
pass
@abc.abstractmethod
@@ -478,6 +479,150 @@ class MetadataReader(BaseReader):
retries=self.retries)
+# Convert OpenStack ConfigDrive NetworkData json to network_config yaml
+def convert_net_json(network_json=None, known_macs=None):
+ """Return a dictionary of network_config by parsing provided
+ OpenStack ConfigDrive NetworkData json format
+
+ OpenStack network_data.json provides a 3 element dictionary
+ - "links" (links are network devices, physical or virtual)
+ - "networks" (networks are ip network configurations for one or more
+ links)
+ - services (non-ip services, like dns)
+
+ networks and links are combined via network items referencing specific
+ links via a 'link_id' which maps to a links 'id' field.
+
+ To convert this format to network_config yaml, we first iterate over the
+ links and then walk the network list to determine if any of the networks
+ utilize the current link; if so we generate a subnet entry for the device
+
+ We also need to map network_data.json fields to network_config fields. For
+ example, the network_data links 'id' field is equivalent to network_config
+ 'name' field for devices. We apply more of this mapping to the various
+ link types that we encounter.
+
+ There are additional fields that are populated in the network_data.json
+ from OpenStack that are not relevant to network_config yaml, so we
+ enumerate a dictionary of valid keys for network_yaml and apply filtering
+ to drop these superflous keys from the network_config yaml.
+ """
+ if network_json is None:
+ return None
+
+ # dict of network_config key for filtering network_json
+ valid_keys = {
+ 'physical': [
+ 'name',
+ 'type',
+ 'mac_address',
+ 'subnets',
+ 'params',
+ 'mtu',
+ ],
+ 'subnet': [
+ 'type',
+ 'address',
+ 'netmask',
+ 'broadcast',
+ 'metric',
+ 'gateway',
+ 'pointopoint',
+ 'scope',
+ 'dns_nameservers',
+ 'dns_search',
+ 'routes',
+ ],
+ }
+
+ links = network_json.get('links', [])
+ networks = network_json.get('networks', [])
+ services = network_json.get('services', [])
+
+ config = []
+ for link in links:
+ subnets = []
+ cfg = {k: v for k, v in link.items()
+ if k in valid_keys['physical']}
+ # 'name' is not in openstack spec yet, but we will support it if it is
+ # present. The 'id' in the spec is currently implemented as the host
+ # nic's name, meaning something like 'tap-adfasdffd'. We do not want
+ # to name guest devices with such ugly names.
+ if 'name' in link:
+ cfg['name'] = link['name']
+
+ for network in [n for n in networks
+ if n['link'] == link['id']]:
+ subnet = {k: v for k, v in network.items()
+ if k in valid_keys['subnet']}
+ if 'dhcp' in network['type']:
+ t = 'dhcp6' if network['type'].startswith('ipv6') else 'dhcp4'
+ subnet.update({
+ 'type': t,
+ })
+ else:
+ subnet.update({
+ 'type': 'static',
+ 'address': network.get('ip_address'),
+ })
+ if network['type'] == 'ipv4':
+ subnet['ipv4'] = True
+ if network['type'] == 'ipv6':
+ subnet['ipv6'] = True
+ subnets.append(subnet)
+ cfg.update({'subnets': subnets})
+ if link['type'] in ['ethernet', 'vif', 'ovs', 'phy', 'bridge']:
+ cfg.update({
+ 'type': 'physical',
+ 'mac_address': link['ethernet_mac_address']})
+ elif link['type'] in ['bond']:
+ params = {}
+ for k, v in link.items():
+ if k == 'bond_links':
+ continue
+ elif k.startswith('bond'):
+ params.update({k: v})
+ cfg.update({
+ 'bond_interfaces': copy.deepcopy(link['bond_links']),
+ 'params': params,
+ })
+ elif link['type'] in ['vlan']:
+ cfg.update({
+ 'name': "%s.%s" % (link['vlan_link'],
+ link['vlan_id']),
+ 'vlan_link': link['vlan_link'],
+ 'vlan_id': link['vlan_id'],
+ 'mac_address': link['vlan_mac_address'],
+ })
+ else:
+ raise ValueError(
+ 'Unknown network_data link type: %s' % link['type'])
+
+ config.append(cfg)
+
+ need_names = [d for d in config
+ if d.get('type') == 'physical' and 'name' not in d]
+
+ if need_names:
+ if known_macs is None:
+ known_macs = net.get_interfaces_by_mac()
+
+ for d in need_names:
+ mac = d.get('mac_address')
+ if not mac:
+ raise ValueError("No mac_address or name entry for %s" % d)
+ if mac not in known_macs:
+ raise ValueError("Unable to find a system nic for %s" % d)
+ d['name'] = known_macs[mac]
+
+ for service in services:
+ cfg = service
+ cfg.update({'type': 'nameserver'})
+ config.append(cfg)
+
+ return {'version': 1, 'config': config}
+
+
def convert_vendordata_json(data, recurse=True):
"""data: a loaded json *object* (strings, arrays, dicts).
return something suitable for cloudinit vendordata_raw.
diff --git a/cloudinit/stages.py b/cloudinit/stages.py
index 5756e74d..47deac6e 100644
--- a/cloudinit/stages.py
+++ b/cloudinit/stages.py
@@ -44,6 +44,7 @@ from cloudinit import helpers
from cloudinit import importer
from cloudinit import log as logging
from cloudinit import net
+from cloudinit.net import cmdline
from cloudinit.reporting import events
from cloudinit import sources
from cloudinit import type_utils
@@ -612,13 +613,13 @@ class Init(object):
if os.path.exists(disable_file):
return (None, disable_file)
- cmdline_cfg = ('cmdline', net.read_kernel_cmdline_config())
+ cmdline_cfg = ('cmdline', cmdline.read_kernel_cmdline_config())
dscfg = ('ds', None)
if self.datasource and hasattr(self.datasource, 'network_config'):
dscfg = ('ds', self.datasource.network_config)
sys_cfg = ('system_cfg', self.cfg.get('network'))
- for loc, ncfg in (cmdline_cfg, dscfg, sys_cfg):
+ for loc, ncfg in (cmdline_cfg, sys_cfg, dscfg):
if net.is_disabled_cfg(ncfg):
LOG.debug("network config disabled by %s", loc)
return (None, loc)
diff --git a/cloudinit/templater.py b/cloudinit/templater.py
index a9231482..41ef27e3 100644
--- a/cloudinit/templater.py
+++ b/cloudinit/templater.py
@@ -3,10 +3,12 @@
# Copyright (C) 2012 Canonical Ltd.
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
# Copyright (C) 2012 Yahoo! Inc.
+# Copyright (C) 2016 Amazon.com, Inc. or its affiliates.
#
# Author: Scott Moser <scott.moser@canonical.com>
# Author: Juerg Haefliger <juerg.haefliger@hp.com>
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
+# Author: Andrew Jorgensen <ajorgens@amazon.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
@@ -102,12 +104,11 @@ def detect_template(text):
rest = ''
type_match = TYPE_MATCHER.match(ident)
if not type_match:
- if not CHEETAH_AVAILABLE:
- LOG.warn("Cheetah not available as the default renderer for"
- " unknown template, reverting to the basic renderer.")
- return ('basic', basic_render, text)
- else:
+ if CHEETAH_AVAILABLE:
+ LOG.debug("Using Cheetah as the renderer for unknown template.")
return ('cheetah', cheetah_render, text)
+ else:
+ return ('basic', basic_render, text)
else:
template_type = type_match.group(1).lower().strip()
if template_type not in ('jinja', 'cheetah', 'basic'):
@@ -142,6 +143,11 @@ def render_to_file(fn, outfn, params, mode=0o644):
util.write_file(outfn, contents, mode=mode)
+def render_string_to_file(content, outfn, params, mode=0o644):
+ contents = render_string(content, params)
+ util.write_file(outfn, contents, mode=mode)
+
+
def render_string(content, params):
if not params:
params = {}
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 8d6cbb4b..e5dd61a0 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -171,7 +171,8 @@ class ProcessExecutionError(IOError):
def __init__(self, stdout=None, stderr=None,
exit_code=None, cmd=None,
- description=None, reason=None):
+ description=None, reason=None,
+ errno=None):
if not cmd:
self.cmd = '-'
else:
@@ -202,6 +203,7 @@ class ProcessExecutionError(IOError):
else:
self.reason = '-'
+ self.errno = errno
message = self.MESSAGE_TMPL % {
'description': self.description,
'cmd': self.cmd,
@@ -336,6 +338,16 @@ def rand_str(strlen=32, select_from=None):
return "".join([random.choice(select_from) for _x in range(0, strlen)])
+def rand_dict_key(dictionary, postfix=None):
+ if not postfix:
+ postfix = ""
+ while True:
+ newkey = rand_str(strlen=8) + "_" + postfix
+ if newkey not in dictionary:
+ break
+ return newkey
+
+
def read_conf(fname):
try:
return load_yaml(load_file(fname), default={})
@@ -1147,7 +1159,14 @@ def find_devs_with(criteria=None, oformat='device',
options.append(path)
cmd = blk_id_cmd + options
# See man blkid for why 2 is added
- (out, _err) = subp(cmd, rcs=[0, 2])
+ try:
+ (out, _err) = subp(cmd, rcs=[0, 2])
+ except ProcessExecutionError as e:
+ if e.errno == errno.ENOENT:
+ # blkid not found...
+ out = ""
+ else:
+ raise
entries = []
for line in out.splitlines():
line = line.strip()
@@ -1696,7 +1715,8 @@ def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
sp = subprocess.Popen(args, **kws)
(out, err) = sp.communicate(data)
except OSError as e:
- raise ProcessExecutionError(cmd=args, reason=e)
+ raise ProcessExecutionError(cmd=args, reason=e,
+ errno=e.errno)
rc = sp.returncode
if rc not in rcs:
raise ProcessExecutionError(stdout=out, stderr=err,
@@ -2190,7 +2210,7 @@ def _call_dmidecode(key, dmidecode_path):
return ""
return result
except (IOError, OSError) as _err:
- LOG.debug('failed dmidecode cmd: %s\n%s', cmd, _err.message)
+ LOG.debug('failed dmidecode cmd: %s\n%s', cmd, _err)
return None