diff --git a/virt-install b/virt-install index 3b4ecd2f..bf5219a4 100755 --- a/virt-install +++ b/virt-install @@ -456,6 +456,9 @@ def build_installer(options, guest, installdata): installer.set_initrd_injections(options.initrd_inject) if options.autostart: installer.autostart = True + if options.cloud_init: + cloudinit_data = cli.parse_cloud_init(options.cloud_init) + installer.set_cloudinit_data(cloudinit_data) return installer @@ -842,6 +845,8 @@ def parse_args(): help=_("Perform an unattended installation")) insg.add_argument("--install", help=_("Specify fine grained install options")) + insg.add_argument("--cloud-init", nargs="?", const=1, + help=_("Perform a cloud image installation, configuring cloud-init")) # Takes a URL and just prints to stdout the detected distro name insg.add_argument("--test-media-detection", help=argparse.SUPPRESS) diff --git a/virtinst/cli.py b/virtinst/cli.py index 369ed115..ce4095f4 100644 --- a/virtinst/cli.py +++ b/virtinst/cli.py @@ -28,6 +28,7 @@ from .nodedev import NodeDevice from .osdict import OSDB from .storage import StoragePool, StorageVolume from .install.unattended import UnattendedData +from .install.cloudinit import CloudInitData HAS_VIRTVIEWER = shutil.which("virt-viewer") @@ -467,7 +468,7 @@ def get_meter(): def _get_completer_parsers(): return VIRT_PARSERS + [ParserCheck, ParserLocation, - ParserUnattended, ParserInstall] + ParserUnattended, ParserInstall, ParserCloudInit] def _virtparser_completer(prefix, **kwargs): @@ -1614,6 +1615,31 @@ def parse_install(optstr): return installdata +######################## +# --cloud-init parsing # +######################## + +class ParserCloudInit(VirtCLIParser): + cli_arg_name = "cloud_init" + supports_clearxml = False + + @classmethod + def _init_class(cls, **kwargs): + VirtCLIParser._init_class(**kwargs) + cls.add_arg("root-password", "root_password") + + +def parse_cloud_init(optstr): + ret = CloudInitData() + if optstr == 1: + # This means bare --cloud-init, so there's nothing to parse + return ret + + parser = ParserCloudInit(optstr) + parser.parse(ret) + return ret + + ###################### # --location parsing # ###################### diff --git a/virtinst/install/cloudinit.py b/virtinst/install/cloudinit.py new file mode 100644 index 00000000..41667f4b --- /dev/null +++ b/virtinst/install/cloudinit.py @@ -0,0 +1,57 @@ +import tempfile +import random +import string +import time +from ..logger import log + + +class CloudInitData(): + root_password = None + + +def create_metadata(scratchdir, hostname=None): + if hostname: + instance = hostname + else: + hostname = instance = "localhost" + content = 'instance-id: %s\n' % instance + content += 'hostname: %s\n' % hostname + log.debug("Generated cloud-init metadata:\n%s", content) + + fileobj = tempfile.NamedTemporaryFile( + prefix="virtinst-", suffix="-metadata", + dir=scratchdir, delete=False) + filename = fileobj.name + + with open(filename, "w") as f: + f.write(content) + return filename + + +def create_userdata(scratchdir, cloudinit_data, username=None, password=None): + if not password: + password = "" + for dummy in range(16): + password += random.choice(string.ascii_letters + string.digits) + content = "#cloud-config\n" + if username: + content += "name: %s\n" % username + if cloudinit_data.root_password == "generate": + pass + else: + content += "password: %s\n" % password + log.debug("Generated password for first boot: \n%s", password) + time.sleep(20) + content += "runcmd:\n" + content += "- [ sudo, touch, /etc/cloud/cloud-init.disabled ]\n" + log.debug("Generated cloud-init userdata:\n%s", content) + + + fileobj = tempfile.NamedTemporaryFile( + prefix="virtinst-", suffix="-userdata", + dir=scratchdir, delete=False) + filename = fileobj.name + + with open(filename, "w+") as f: + f.write(content) + return filename diff --git a/virtinst/install/installer.py b/virtinst/install/installer.py index 6fa0d40a..1c31871e 100644 --- a/virtinst/install/installer.py +++ b/virtinst/install/installer.py @@ -16,6 +16,7 @@ from ..devices import DeviceDisk from ..osdict import OSDB from ..logger import log from .. import progress +from .cloudinit import create_metadata, create_userdata def _make_testsuite_path(path): @@ -60,6 +61,7 @@ class Installer(object): self._tmpfiles = [] self._defaults_are_set = False self._unattended_data = None + self._cloudinit_data = None self._install_bootdev = install_bootdev self._no_install = no_install @@ -279,6 +281,9 @@ class Installer(object): elif unattended_scripts: self._prepare_unattended_data(guest, meter, unattended_scripts) + elif self._cloudinit_data: + self._install_cloudinit(guest) + def _cleanup(self, guest): if self._treemedia: self._treemedia.cleanup(guest) @@ -414,6 +419,18 @@ class Installer(object): def set_unattended_data(self, unattended_data): self._unattended_data = unattended_data + def set_cloudinit_data(self, cloudinit_data): + self._cloudinit_data = cloudinit_data + + def _install_cloudinit(self, guest): + metadata = create_metadata(guest.conn.get_app_cache_dir()) + userdata = create_userdata(guest.conn.get_app_cache_dir(), self._cloudinit_data) + + iso = perform_cdrom_injections([(metadata, "meta-data"), (userdata, "user-data")], + guest.conn.get_app_cache_dir(), cloudinit=True) + self._tmpfiles.append(iso) + self._add_unattended_install_cdrom_device(guest, iso) + ########################## # guest install handling # diff --git a/virtinst/install/installerinject.py b/virtinst/install/installerinject.py index d7cfcfb4..7ad4833f 100644 --- a/virtinst/install/installerinject.py +++ b/virtinst/install/installerinject.py @@ -44,19 +44,21 @@ def _run_initrd_commands(initrd, tempdir): log.debug("gzip stderr=%s", gziperr) -def _run_iso_commands(iso, tempdir): +def _run_iso_commands(iso, tempdir, cloudinit=False): cmd = ["genisoimage", "-o", iso, "-J", "-input-charset", "utf8", - "-rational-rock", - tempdir] + "-rational-rock"] + if cloudinit: + cmd.extend(["-V", "cidata"]) + cmd.append(tempdir) log.debug("Running iso build command: %s", cmd) output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) log.debug("cmd output: %s", output) -def _perform_generic_injections(injections, scratchdir, media, cb): +def _perform_generic_injections(injections, scratchdir, media, cb, cloudinit=False): if not injections: return @@ -74,20 +76,20 @@ def _perform_generic_injections(injections, scratchdir, media, cb): filename, dst, media) shutil.copy(filename, os.path.join(tempdir, dst)) - return cb(media, tempdir) + return cb(media, tempdir, cloudinit) finally: shutil.rmtree(tempdir) -def perform_initrd_injections(initrd, injections, scratchdir): +def perform_initrd_injections(initrd, injections, scratchdir, cloudinit=False): """ Insert files into the root directory of the initial ram disk """ _perform_generic_injections(injections, scratchdir, initrd, - _run_initrd_commands) + _run_initrd_commands, cloudinit) -def perform_cdrom_injections(injections, scratchdir): +def perform_cdrom_injections(injections, scratchdir, cloudinit=False): """ Insert files into the root directory of a generated cdrom """ @@ -98,7 +100,7 @@ def perform_cdrom_injections(injections, scratchdir): try: _perform_generic_injections(injections, scratchdir, iso, - _run_iso_commands) + _run_iso_commands, cloudinit) except Exception: # pragma: no cover os.unlink(iso) raise