diff options
-rwxr-xr-x | cloud-init.py | 31 | ||||
-rw-r--r-- | cloudinit/DataSource.py | 41 | ||||
-rw-r--r-- | cloudinit/DataSourceEc2.py | 51 | ||||
-rw-r--r-- | cloudinit/DataSourceNoCloud.py | 152 | ||||
-rw-r--r-- | cloudinit/__init__.py | 28 | ||||
-rw-r--r-- | cloudinit/util.py | 33 | ||||
-rw-r--r-- | doc/examples/preseed-metadata.txt | 24 | ||||
-rwxr-xr-x | setup.py | 2 | ||||
-rwxr-xr-x | tools/uncloud-init | 138 | ||||
-rw-r--r-- | upstart/cloud-init-local.conf | 9 | ||||
-rw-r--r-- | upstart/cloud-init.conf | 3 |
11 files changed, 459 insertions, 53 deletions
diff --git a/cloud-init.py b/cloud-init.py index ff70017f..11bf89af 100755 --- a/cloud-init.py +++ b/cloud-init.py @@ -30,6 +30,15 @@ def warn(str): sys.stderr.write(str) def main(): + cmds = ( "start", "start-local" ) + cmd = "" + if len(sys.argv) > 1: + cmd = sys.argv[1] + + if not cmd in cmds: + sys.stderr.write("bad command %s. use one of %s\n" % (cmd, cmds)) + sys.exit(1) + now = time.strftime("%a, %d %b %Y %H:%M:%S %z") try: uptimef=open("/proc/uptime") @@ -39,29 +48,39 @@ def main(): warn("unable to open /proc/uptime\n") uptime = "na" - msg = "cloud-init running: %s. up %s seconds" % (now, uptime) + msg = "cloud-init %s running: %s. up %s seconds" % (cmd, now, uptime) sys.stderr.write(msg + "\n") sys.stderr.flush() + source_type = "all" + if cmd == "start-local": + source_type = "local" + cloudinit.logging_set_from_cfg_file() log = logging.getLogger() log.info(msg) # cache is not instance specific, so it has to be purged - cloudinit.purge_cache() + # but we want 'start' to benefit from a cache if + # a previous start-local populated one + if cmd == "start-local": + cloudinit.purge_cache() - cloud = cloudinit.CloudInit() + cloud = cloudinit.CloudInit(source_type=source_type) try: cloud.get_data_source() - except Exception as e: - print e - sys.stderr.write("Failed to get instance data\n") + except cloudinit.DataSourceNotFoundException as e: + sys.stderr.write("no instance data found in %s\n" % cmd) sys.exit(1) # store the metadata cloud.update_cache() + msg = "found data source: %s" % cloud.datasource + sys.stderr.write(msg + "\n") + log.debug(msg) + # parse the user data (ec2-run-userdata.py) try: cloud.sem_and_run("consume_userdata", "once-per-instance", diff --git a/cloudinit/DataSource.py b/cloudinit/DataSource.py index 858608cc..d1458ffd 100644 --- a/cloudinit/DataSource.py +++ b/cloudinit/DataSource.py @@ -36,7 +36,20 @@ class DataSource: return(self.userdata_raw) def get_public_ssh_keys(self): - return([]) + keys = [] + if not self.metadata.has_key('public-keys'): return([]) + for keyname, klist in self.metadata['public-keys'].items(): + # lp:506332 uec metadata service responds with + # data that makes boto populate a string for 'klist' rather + # than a list. + if isinstance(klist,str): + klist = [ klist ] + for pkey in klist: + # there is an empty string at the end of the keylist, trim it + if pkey: + keys.append(pkey) + + return(keys) def device_name_to_device(self, name): # translate a 'name' to a device @@ -45,3 +58,29 @@ class DataSource: # ephemeral0: sdb # and return 'sdb' for input 'ephemeral0' return(None) + + def get_locale(self): + return('en_US.UTF-8') + + def get_local_mirror(self): + return('http://archive.ubuntu.com/ubuntu/') + + def get_instance_id(self): + if 'instance-id' not in self.metadata: + return "ubuntuhost" + return(self.metadata['instance-id']) + + def get_hostname(self): + if not 'local-hostname' in self.metadata: + return None + + toks = self.metadata['local-hostname'].split('.') + # if there is an ipv4 address in 'local-hostname', then + # make up a hostname (LP: #475354) + if len(toks) == 4: + try: + r = filter(lambda x: int(x) < 256 and x > 0, toks) + if len(r) == 4: + return("ip-%s" % '-'.join(r)) + except: pass + return toks[0] diff --git a/cloudinit/DataSourceEc2.py b/cloudinit/DataSourceEc2.py index cf50a3d5..8a79eb5f 100644 --- a/cloudinit/DataSourceEc2.py +++ b/cloudinit/DataSourceEc2.py @@ -19,12 +19,14 @@ import DataSource import cloudinit +import cloudinit.util as util import socket import urllib2 import time import sys import boto_utils import os.path +import errno class DataSourceEc2(DataSource.DataSource): api_ver = '2009-04-04' @@ -39,21 +41,20 @@ class DataSourceEc2(DataSource.DataSource): def __init__(self): pass + def __str__(self): + return("DataSourceEc2") + def get_data(self): try: - udf = open(self.cachedir + "/user-data.raw") - self.userdata_raw = udf.read() - udf.close() - - mdf = open(self.cachedir + "/meta-data.raw") - data = mdf.read() - self.metadata = eval(data) - mdf.close() - + (md,ud) = util.read_seeded(self.cachedir + "/") + self.userdata_raw = ud + self.metadata = md cloudinit.log.debug("using seeded ec2 cache data in %s" % self.cachedir) return True - except: - pass + except OSError, e: + if e.errno != errno.ENOENT: + cloudinit.log.warn("unexpected error from preseeded data") + raise try: if not self.wait_for_metadata_service(): @@ -81,18 +82,6 @@ class DataSourceEc2(DataSource.DataSource): else: return(self.location_locale_map["default"]) - def get_hostname(self): - toks = self.metadata['local-hostname'].split('.') - # if there is an ipv4 address in 'local-hostname', then - # make up a hostname (LP: #475354) - if len(toks) == 4: - try: - r = filter(lambda x: int(x) < 256 and x > 0, toks) - if len(r) == 4: - return("ip-%s" % '-'.join(r)) - except: pass - return toks[0] - def get_mirror_from_availability_zone(self, availability_zone = None): # availability is like 'us-west-1b' or 'eu-west-1a' if availability_zone == None: @@ -138,22 +127,6 @@ class DataSourceEc2(DataSource.DataSource): int(time.time()-starttime)) return False - def get_public_ssh_keys(self): - keys = [] - if not self.metadata.has_key('public-keys'): return([]) - for keyname, klist in self.metadata['public-keys'].items(): - # lp:506332 uec metadata service responds with - # data that makes boto populate a string for 'klist' rather - # than a list. - if isinstance(klist,str): - klist = [ klist ] - for pkey in klist: - # there is an empty string at the end of the keylist, trim it - if pkey: - keys.append(pkey) - - return(keys) - def device_name_to_device(self, name): # consult metadata service, that has # ephemeral0: sdb diff --git a/cloudinit/DataSourceNoCloud.py b/cloudinit/DataSourceNoCloud.py new file mode 100644 index 00000000..573f3f43 --- /dev/null +++ b/cloudinit/DataSourceNoCloud.py @@ -0,0 +1,152 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2009-2010 Canonical Ltd. +# +# Author: Scott Moser <scott.moser@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/>. + +import DataSource + +import cloudinit +import cloudinit.util as util +import sys +import os.path +import os +import errno + +class DataSourceNoCloud(DataSource.DataSource): + metadata = None + userdata = None + userdata_raw = None + supported_seed_starts = ( "/" , "file://" ) + seed = None + cmdline_id = "ds=nocloud" + seeddir = cloudinit.cachedir + '/nocloud' + + def __init__(self): + pass + + def __str__(self): + mstr="DataSourceNoCloud" + mstr = mstr + " [seed=%s]" % self.seed + return(mstr) + + def get_data(self): + defaults = { + "local-hostname" : "ubuntuhost", + "instance-id" : "nocloud" + } + + md = { } + ud = "" + + try: + # parse the kernel command line, getting data passed in + md = parse_cmdline_data(self.cmdline_id) + except: + util.logexc(cloudinit.log,util.WARN) + return False + + # check to see if the seeddir has data. + try: + (seeddir_md,ud) = util.read_seeded(self.seeddir + "/") + self.metadata = md + md = util.mergedict(md,seeddir_md) + cloudinit.log.debug("using seeded cache data in %s" % self.seeddir) + except OSError, e: + if e.errno != errno.ENOENT: + util.logexc(cloudinit.log,util.WARN) + raise + + # there was no indication on kernel cmdline or data + # in the seeddir suggesting this handler should be used. + if md is None: + return False + + # the special argument "seedfrom" indicates we should + # attempt to seed the userdata / metadata from its value + if "seedfrom" in md: + seedfrom = md["seedfrom"] + found = False + for proto in self.supported_seed_starts: + if seedfrom.startswith(proto): + found=proto + break + if not found: + cloudinit.log.debug("seed from %s not supported by %s" % + (seedfrom, self.__class__)) + return False + + # this could throw errors, but the user told us to do it + # so if errors are raised, let them raise + (md_seed,ud) = util.read_seeded(seedfrom) + cloudinit.log.debug("using seeded cache data from %s" % seedfrom) + + # values in the command line override those from the seed + md = util.mergedict(md,md_seed) + self.seed = seedfrom + + md = util.mergedict(md,defaults) + self.metadata = md; + self.userdata_raw = ud + return True + +# returns a dictionary of key/val or None if cmdline did not have data +# example cmdline: +# root=LABEL=uec-rootfs ro ds=nocloud +def parse_cmdline_data(ds_id,cmdline=None): + if cmdline is None: + if 'DEBUG_PROC_CMDLINE' in os.environ: + cmdline = os.environ["DEBUG_PROC_CMDLINE"] + else: + cmdfp = open("/proc/cmdline") + cmdline = cmdfp.read().strip() + cmdfp.close() + cmdline = " %s " % cmdline.lower() + + if not ( " %s " % ds_id in cmdline or " %s;" % ds_id in cmdline ): + return None + + argline="" + # cmdline can contain: + # ds=nocloud[;key=val;key=val] + for tok in cmdline.split(): + if tok.startswith(ds_id): argline=tok.split("=",1) + + # argline array is now 'nocloud' followed optionally by + # a ';' and then key=value pairs also terminated with ';' + tmp=argline[1].split(";") + if len(tmp) > 1: + kvpairs=tmp[1:] + else: + kvpairs=() + + # short2long mapping to save cmdline typing + s2l = { "h" : "local-hostname", "i" : "instance-id", "s" : "seedfrom" } + cfg = { } + for item in kvpairs: + try: + (k,v) = item.split("=",1) + except: + k=item + v=None + if k in s2l: k=s2l[k] + cfg[k]=v + + return(cfg) + +class DataSourceNoCloudNet(DataSourceNoCloud): + cmdline_id = "ds=nocloud-net" + supported_seed_starts = ( "http://", "https://", "ftp://" ) + seeddir = cloudinit.cachedir + '/nocloud-net' diff --git a/cloudinit/__init__.py b/cloudinit/__init__.py index a5df3436..830bc17b 100644 --- a/cloudinit/__init__.py +++ b/cloudinit/__init__.py @@ -159,20 +159,27 @@ def logging_set_from_cfg(cfg, logfile=None): logging.config.fileConfig(StringIO.StringIO(failsafe)) import DataSourceEc2 +import DataSourceNoCloud import UserDataHandler class CloudInit: datasource_map = { "ec2" : DataSourceEc2.DataSourceEc2, + "nocloud" : DataSourceNoCloud.DataSourceNoCloud, + "nocloud-net" : DataSourceNoCloud.DataSourceNoCloudNet } datasource = None - auto_order = [ 'ec2' ] + auto_orders = { + "all": ( "nocloud-net", "ec2" ), + "local" : ( "nocloud", ), + } cfg = None part_handlers = { } old_conffile = '/etc/ec2-init/ec2-config.cfg' + source_type = "all" - def __init__(self, sysconfig=system_config): + def __init__(self, source_type = "all", sysconfig=system_config): self.part_handlers = { 'text/x-shellscript' : self.handle_user_script, 'text/cloud-config' : self.handle_cloud_config, @@ -182,6 +189,7 @@ class CloudInit: } self.sysconfig=sysconfig self.cfg=self.read_cfg() + self.source_type = source_type def read_cfg(self): if self.cfg: @@ -232,18 +240,21 @@ class CloudInit: if self.datasource is not None: return True if self.restore_from_cache(): + log.debug("restored from cache type %s" % self.datasource) return True dslist=[ ] cfglist=self.cfg['cloud_type'] if cfglist == "auto": - dslist = self.auto_order + dslist = self.auto_orders[self.source_type] elif cfglist: for ds in cfglist.split(','): dslist.append(strip(ds).tolower()) for ds in dslist: - if ds not in self.datasource_map: continue + if ds not in self.datasource_map: + log.warn("data source %s not found in map" % ds) + continue try: s = self.datasource_map[ds]() if s.get_data(): @@ -252,9 +263,11 @@ class CloudInit: log.debug("found data source %s" % ds) return True except Exception as e: + log.warn("get_data of %s raised %s" % (ds,e)) + util.logexc(log) pass - log.critical("Could not find data source") - raise Exception("Could not find data source") + log.debug("did not find data source from %s" % dslist) + raise DataSourceNotFoundException("Could not find data source") def get_userdata(self): return(self.datasource.get_userdata()) @@ -475,3 +488,6 @@ def purge_cache(): except: return(False) return(True) + +class DataSourceNotFoundException(Exception): + pass diff --git a/cloudinit/util.py b/cloudinit/util.py index 7f5c1db4..137921ed 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -21,6 +21,13 @@ import errno import subprocess from Cheetah.Template import Template import cloudinit +import urllib2 +import logging +import traceback + +WARN = logging.WARN +DEBUG = logging.DEBUG +INFO = logging.INFO def read_conf(fname): try: @@ -114,3 +121,29 @@ def render_to_file(template, outfile, searchList): f.write(t.respond()) f.close() +# raise OSError with enoent if not found +def read_seeded(base="", ext=".raw", timeout=2): + if base.startswith("/"): + base="file://%s" % base + + ud_url = "%s%s%s" % (base, "user-data", ext) + md_url = "%s%s%s" % (base, "meta-data", ext) + + try: + md_resp = urllib2.urlopen(urllib2.Request(md_url), timeout=timeout) + ud_resp = urllib2.urlopen(urllib2.Request(ud_url), timeout=timeout) + + md_str = md_resp.read() + ud = ud_resp.read() + md = yaml.load(md_str) + + return(md,ud) + except urllib2.HTTPError: + raise + except urllib2.URLError, e: + if isinstance(e.reason,OSError) and e.reason.errno == errno.ENOENT: + raise e.reason + raise e + +def logexc(log,lvl=logging.DEBUG): + log.log(lvl,traceback.format_exc()) diff --git a/doc/examples/preseed-metadata.txt b/doc/examples/preseed-metadata.txt new file mode 100644 index 00000000..62156826 --- /dev/null +++ b/doc/examples/preseed-metadata.txt @@ -0,0 +1,24 @@ +# this is yaml formated data +# it is expected to be roughly what you would get from running the following +# on an ec2 instance: +# python -c 'import boto.utils, yaml; print(yaml.dump(boto.utils.get_instance_metadata()))' +ami-id: ami-fd4aa494 +ami-launch-index: '0' +ami-manifest-path: ubuntu-images-us/ubuntu-lucid-10.04-amd64-server-20100427.1.manifest.xml +block-device-mapping: {ami: sda1, ephemeral0: sdb, ephemeral1: sdc, root: /dev/sda1} +hostname: domU-12-31-38-07-19-44.compute-1.internal +instance-action: none +instance-id: i-87018aed +instance-type: m1.large +kernel-id: aki-c8b258a1 +local-hostname: domU-12-31-38-07-19-44.compute-1.internal +local-ipv4: 10.223.26.178 +placement: {availability-zone: us-east-1d} +public-hostname: ec2-184-72-174-120.compute-1.amazonaws.com +public-ipv4: 184.72.174.120 +public-keys: + ec2-keypair.us-east-1: [ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCD9dlT00vOUC8Ttq6YH8RzUCVqPQl6HaSfWSTKYnZiVCpTBj1CaRZPLRLmkSB9Nziy4aRJa/LZMbBHXytQKnB1psvNknqC2UNlrXXMk+Vx5S4vg21MXYYimK4uZEY0Qz29QUiTyNsx18jpAaF4ocUpTpRhxPEBCcSCDmMbc27MU2XuTbasM2NjW/w0bBF3ZFhdH68dZICXdTxS2jUrtrCnc1D/QXVZ5kQO3jsmSyJg8E0nE+6Onpx2YRoVRSwjpGzVZ+BlXPnN5xBREBG8XxzhNFHJbek+RgK5TfL+k4yD4XhnVZuZu53cBAFhj+xPKhtisSd+YmaEq+Jt9uS0Ekd5 + ec2-keypair.us-east-1, ''] +reservation-id: r-e2225889 +security-groups: default + @@ -38,6 +38,8 @@ setup(name='cloud-init', ('/etc/cloud/templates', glob('templates/*')), ('/etc/init', glob('upstart/*.conf')), ('/usr/share/cloud-init', []), + ('/usr/lib/cloud-init', + ['tools/uncloud-init','tools/write-mime-multipart']), ('/usr/share/doc/cloud-init', glob('doc/*.txt')), ('/usr/share/doc/cloud-init/examples', glob('doc/examples/*')), ], diff --git a/tools/uncloud-init b/tools/uncloud-init new file mode 100755 index 00000000..c0bc0b4f --- /dev/null +++ b/tools/uncloud-init @@ -0,0 +1,138 @@ +#!/bin/sh +# vi: ts=4 noexpandtab + +# This script is meant to "kvmify" an image. Its not meant to be +# terribly robust, or a good idea to ever run in a "real image". +# its' only intended method of invocation is from the kernel as 'init' +# in which case it will then invoke /sbin/init after it is done +# init=/path/to/kvmify-init + +KEY="xupdate" +UMOUNT="" +RMDIR="" +MARK=/root/uncloud-init-ran +ROOT_RW="" + +doexec() { + [ -z "$ROOT_RW" ] || date > "${MARK}"; + cleanup; + log "invoking /sbin/init $*" + exec /sbin/init "$@"; +} +log() { echo "::${0##*/}:" "$@"; } +cleanup() { + [ -z "${UMOUNT}" ] || { umount "${UMOUNT}" && unset UMOUNT; } + [ -z "${RMDIR}" ] || { rm -Rf "${RMDIR}" && unset RMDIR; } + [ -z "${ROOT_RW}" ] || { mount -o remount,ro / ; unset ROOT_RW; } +} + +updateFrom() { + local dev=$1 fmt=$2 + local mp=""; + + [ "${fmt}" = "tar" -o "${fmt}" = "mnt" ] || + { log FAIL "unknown format ${fmt}"; return 1; } + + log INFO "updating from ${dev} format ${fmt}" + [ ! -e "${dev}" -a -e "/dev/${dev}" ] && dev="/dev/${dev}" + [ -e "${dev}" ] || { echo "no file $dev"; return 2; } + + mp=$(mktemp -d "${TEMPDIR:-/tmp}/update.XXXXXX") && + RMDIR="${mp}" || + { log FAIL "failed to mktemp"; return 1; } + + if [ "$fmt" = "tar" ]; then + dd "if=${dev}" | ( tar -C "${mp}" -xf - ) || + { log FAIL "failed to extract ${dev}"; return 1; } + elif [ "$fmt" = "mnt" ]; then + mount -o ro "${dev}" "${mp}" && UMOUNT=${mp} || + { log FAIL "failed mount ${mp}"; return 1; } + else + log FAIL "unknown format ${fmt}"; return 1; + fi + + if [ -d "${mp}/updates" ]; then + rsync -av "${mp}/updates/" "/" || + { log FAIL "failed rsync updates/ /"; return 1; } + fi + if [ -d "${mp}/updates.tar" ]; then + tar -C / -xvf "${mp}/updates.tar" || + { log FAIL "failed tar -C / -xvf ${mp}/updates.tar"; return 1; } + fi + script="${mp}/updates.script" + if [ -f "${script}" -a -x "${script}" ]; then + MP_DIR=${mp} "${mp}/updates.script" || + { log FAIL "failed to run updates.script"; return 1; } + fi +} + +fail() { { [ $# -eq 0 ] && log "FAILING" ; } || log "$@"; exit 1; } + +[ -s "$MARK" ] && { log "already updated" ; doexec "$@"; } + +mount -o remount,rw / || fail "failed to mount rw" +ROOT_RW=1 + +if [ ! -e /proc/cmdline ]; then + mount -t proc /proc /proc + read cmdline < /proc/cmdline + umount /proc +else + read cmdline < /proc/cmdline +fi + +ubuntu_pass="" + +for x in ${cmdline}; do + case "$x" in + ${KEY}=*) + val=${x#${KEY}=} + dev=${val%:*} + [ "${dev}" = "${val}" ] && fmt="" || fmt=${val#${dev}:} + log "update from ${dev},${fmt}" + updateFrom "${dev}" "${fmt}" || fail "update failed" + log "end update ${dev},${fmt}" + ;; + ubuntu-pass=*|ubuntu_pass=*) ubuntu_pass=${x#*=};; + helpmount) helpmount=1;; + root=*) rootspec=${x#root=};; + esac +done + +if [ "${ubuntu_pass}" = "R" -o "${ubuntu_pass}" = "random" ]; then + ubuntu_pass=$(python -c 'import string, random; +random.seed(); print "".join(random.sample(string.letters+string.digits, 8))') + log "settting ubuntu pass = ${ubuntu_pass}" + printf "\n===\nubuntu_pass = %s\n===\n" "${ubuntu_pass}" >/dev/ttyS0 +fi + +[ -z "${ubuntu_pass}" ] || + printf "ubuntu:%s\n" "${ubuntu_pass}" > /root/ubuntu-user-pass + +if [ -e /root/ubuntu-user-pass ]; then + log "changing ubuntu user's password!" + chpasswd < /root/ubuntu-user-pass || + log "FAIL: failed changing pass" +fi + +cp /etc/init/tty2.conf /etc/init/ttyS0.conf && + sed -i s,tty2,ttyS0,g /etc/init/ttyS0.conf 2>/dev/null && + log "enabled console on ttyS0" + +pa=PasswordAuthentication +sed -i "s,${pa} no,${pa} yes," /etc/ssh/sshd_config 2>/dev/null && + log "enabled passwd auth in ssh" || + log "failed to enable passwd ssh" + +grep -q vga16fb /etc/modprobe.d/blacklist.conf || { + echo "blacklist vga16fb" >> /etc/modprobe.d/blacklist.conf && + log "blacklisted vga16fb" +} + +#lstr="${rootspec}" +#if ! grep -q "^${lstr}[[:space:]]" /etc/fstab; then +# log "changing / in /etc/ftab to agree with cmdline (${lstr}) (bug 509841)" +# sed -i "s,^\([^[[:space:]#]\+\)\([[:space:]]\+\)/\([[:space:]]\+\),${lstr}\2/\3," /etc/fstab +#fi + +doexec "$@" diff --git a/upstart/cloud-init-local.conf b/upstart/cloud-init-local.conf new file mode 100644 index 00000000..b6eb21b4 --- /dev/null +++ b/upstart/cloud-init-local.conf @@ -0,0 +1,9 @@ +# cloud-init - the initial cloud-init job +# crawls metadata service, emits cloud-config +start on mounted MOUNTPOINT=/ + +task + +console output + +exec /usr/bin/cloud-init start-local diff --git a/upstart/cloud-init.conf b/upstart/cloud-init.conf index 2a066105..cb2b437b 100644 --- a/upstart/cloud-init.conf +++ b/upstart/cloud-init.conf @@ -1,6 +1,7 @@ # cloud-init - the initial cloud-init job # crawls metadata service, emits cloud-config -start on (mounted MOUNTPOINT=/ and net-device-up IFACE=eth0) +start on (mounted MOUNTPOINT=/ and net-device-up IFACE=eth0 and \ + stopped cloud-init-local ) task |