# This file is part of cloud-init. See LICENSE file for license information.
"""Base NoCloud KVM instance."""
import copy
import os
import socket
import subprocess
import time
import uuid
from ..instances import Instance
from cloudinit.atomic_helper import write_json
from cloudinit import util as c_util
from tests.cloud_tests import LOG, util
# This domain contains reverse lookups for hostnames that are used.
# The primary reason is so sudo will return quickly when it attempts
# to look up the hostname. i9n is just short for 'integration'.
# see also bug 1730744 for why we had to do this.
CI_DOMAIN = "i9n.cloud-init.io"
class NoCloudKVMInstance(Instance):
"""NoCloud KVM backed instance."""
platform_name = "nocloud-kvm"
def __init__(self, platform, name, image_path, properties, config,
features, user_data, meta_data):
"""Set up instance.
@param platform: platform object
@param name: image path
@param image_path: path to disk image to boot.
@param properties: dictionary of properties
@param config: dictionary of configuration values
@param features: dictionary of supported feature flags
"""
super(NoCloudKVMInstance, self).__init__(
platform, name, properties, config, features
)
self.user_data = user_data
if meta_data:
meta_data = copy.deepcopy(meta_data)
else:
meta_data = {}
if 'instance-id' in meta_data:
iid = meta_data['instance-id']
else:
iid = str(uuid.uuid1())
meta_data['instance-id'] = iid
self.instance_id = iid
self.ssh_key_file = os.path.join(
platform.config['data_dir'], platform.config['private_key'])
self.ssh_pubkey_file = os.path.join(
platform.config['data_dir'], platform.config['public_key'])
self.ssh_pubkey = None
if self.ssh_pubkey_file:
with open(self.ssh_pubkey_file, "r") as fp:
self.ssh_pubkey = fp.read().rstrip('\n')
if not meta_data.get('public-keys'):
meta_data['public-keys'] = []
meta_data['public-keys'].append(self.ssh_pubkey)
self.ssh_ip = '127.0.0.1'
self.ssh_port = None
self.pid = None
self.pid_file = None
self.console_file = None
self.disk = image_path
self.cache_mode = platform.config.get('cache_mode',
'cache=none,aio=native')
self.meta_data = meta_data
def shutdown(self, wait=True):
"""Shutdown instance."""
if self.pid:
# This relies on _execute which uses sudo over ssh. The ssh
# connection would get killed before sudo exited, so ignore errors.
cmd = ['shutdown', 'now']
try:
self._execute(cmd)
except util.InTargetExecuteError:
pass
self._ssh_close()
if wait:
LOG.debug("Executed shutdown. waiting on pid %s to end",
self.pid)
time_for_shutdown = 120
give_up_at = time.time() + time_for_shutdown
pid_file_path = '/proc/%s' % self.pid
msg = ("pid %s did not exit in %s seconds after shutdown." %
(self.pid, time_for_shutdown))
while True:
if not os.path.exists(pid_file_path):
break
if time.time() > give_up_at:
raise util.PlatformError("shutdown", msg)
self.pid = None
def destroy(self):
"""Clean up instance."""
if self.pid:
try:
c_util.subp(['kill', '-9', self.pid])
except c_util.ProcessExecutionError:
pass
if self.pid_file:
try:
os.remove(self.pid_file)
except Exception:
pass
self.pid = None
self._ssh_close()
super(NoCloudKVMInstance, self).destroy()
def _execute(self, command, stdin=None, env=None):
env_args = []
if env:
env_args = ['env'] + ["%s=%s" for k, v in env.items()]
return self._ssh(['sudo'] + env_args + list(command), stdin=stdin)
def generate_seed(self, tmpdir):
"""Generate nocloud seed from user-data"""
seed_file = os.path.join(tmpdir, '%s_seed.img' % self.name)
user_data_file = os.path.join(tmpdir, '%s_user_data' % self.name)
meta_data_file = os.path.join(tmpdir, '%s_meta_data' % self.name)
with open(user_data_file, "w") as ud_file:
ud_file.write(self.user_data)
# meta-data can be yaml, but more easily pretty printed with json
write_json(meta_data_file, self.meta_data)
c_util.subp(['cloud-localds', seed_file, user_data_file,
meta_data_file])
return seed_file
def get_free_port(self):
"""Get a free port assigned by the kernel."""
s = socket.socket()
s.bind(('', 0))
num = s.getsockname()[1]
s.close()
return num
def start(self, wait=True, wait_for_cloud_init=False):
"""Start instance."""
tmpdir = self.platform.config['data_dir']
seed = self.generate_seed(tmpdir)
self.pid_file = os.path.join(tmpdir, '%s.pid' % self.name)
self.console_file = os.path.join(tmpdir, '%s-console.log' % self.name)
self.ssh_port = self.get_free_port()
cmd = ['./tools/xkvm',
'--disk', '%s,%s' % (self.disk, self.cache_mode),
'--disk', '%s' % seed,
'--netdev', ','.join(['user',
'hostfwd=tcp::%s-:22' % self.ssh_port,
'dnssearch=%s' % CI_DOMAIN]),
'--', '-pidfile', self.pid_file, '-vnc', 'none',
'-m', '2G', '-smp', '2', '-nographic', '-name', self.name,
'-serial', 'file:' + self.console_file]
subprocess.Popen(cmd,
close_fds=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
while not os.path.exists(self.pid_file):
time.sleep(1)
with open(self.pid_file, 'r') as pid_f:
self.pid = pid_f.readlines()[0].strip()
if wait:
self._wait_for_system(wait_for_cloud_init)
def console_log(self):
if not self.console_file:
return b''
with open(self.console_file, "rb") as fp:
return fp.read()
# vi: ts=4 expandtab