Rework the test framework & the clustering test
* Remove the dead code;
* Rework the test types;
* Restore the instance connectivity check;
* Rework the clustering test to support the new node addition workflow;
* Check whether a machine where MicroStack is installed has hardware
  virtualization capabilities for different architectures. If not, use
  software emulation;
  * the host model is used with KVM since the default QEMU CPU models on
    x86_64 are subject to vulnerabilities without certain CPU-specific
    features. This conflicts with being able to use live migration
    reliably across hosts with different CPUs.
* Add a default-source-ip init argument to allow controlling the source
  IP of the installation host that will be used as a control ip or
  compute ip locally.
  * used in the clustering test so that the local host IP on the
    multipass network is used as a control IP instead of the IP
    through which the default gateway is available;
  * the IP through which the default gateway is accessible is
    used as a fallback for default-source-ip;
* Given upstream CI has a low amount of resources allocated per machine
  use LXD to set up a dummy compute node;
  * Set RLIMIT_MEMLOCK to 'unlimited' in the LXD container profile
    (see the discussion in LP: #1906280);
  * set remember_owner to 0 in qemu.conf for libvirt to avoid the
    uses of XATTRS (the root user is used anyway so there is no
    need to remember a file owner), otherwise libvirt errors out
    in an unprivileged LXD container.
* Use numeric versions of OpenStack packages in the python-packages
  section of the openstack-projects part since the resolver change in
  recent versions of pip disallows for constraints dependencies of
  packages that come from a URL or a path.
  https://github.com/pypa/pip/issues/8210
  * The newest released version of pip is always used during builds
    since snapcraft uses venv to set up virtual environments and the
    ensurepip package is invoked such that a pip version shipped with
    the distro version of python is upgraded:
    https://github.com/python/cpython/blob/3.8/Lib/venv/__init__.py#L282-L289
            cmd = [context.env_exe, '-Im', 'ensurepip', '--upgrade',
                                                    '--default-pip']
  * Environment variables are ignored when pip is installed in the venv:
    https://docs.python.org/3/using/cmdline.html#id2 (-I option)
    So there is no way to use the old pip version resolver.
Minor clustering client and add-compute changes:
* use stderr for diagnostic messages;
* use stdout to output the connection string so that it can be easily
  picked up by CLI tools without parsing.
Change-Id: I5cb3872c5d142c34da2c8b073652c67021d9ef55
			
			
This commit is contained in:
		@@ -2,4 +2,11 @@
 | 
			
		||||
 | 
			
		||||
set -ex
 | 
			
		||||
 | 
			
		||||
modprobe -a vhost vhost-net vhost-scsi vhost-vsock vfio nbd dm-mod dm-thin-pool dm-snapshot iscsi-tcp target-core-mod
 | 
			
		||||
# If we are not running in any type of a container, attempt to load the necessary kernel modules and
 | 
			
		||||
# expect them to be present based on external arrangements (e.g. they could be specified in a LXD profile).
 | 
			
		||||
# TODO: this file will go away when strict confinement gets implemented as snapd will load modules
 | 
			
		||||
# that are specified in the microstack-support interface.
 | 
			
		||||
if [[ `systemd-detect-virt --container` == 'none' ]]
 | 
			
		||||
then
 | 
			
		||||
    modprobe -a vhost vhost-net vhost-scsi vhost-vsock vfio nbd dm-mod dm-thin-pool dm-snapshot iscsi-tcp target-core-mod
 | 
			
		||||
fi
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,16 @@ def _get_default_config():
 | 
			
		||||
        'config.monitoring.ipmi': '',
 | 
			
		||||
        'config.services.extra.telegraf': False,
 | 
			
		||||
        'config.monitoring.custom-config': f'{snap_common}/etc/telegraf'
 | 
			
		||||
                                           '/telegraf-microstack.conf'
 | 
			
		||||
                                           '/telegraf-microstack.conf',
 | 
			
		||||
 | 
			
		||||
        # Use emulation by default (with an option to override if KVM is
 | 
			
		||||
        # supported).
 | 
			
		||||
        'config.nova.virt-type': 'qemu',
 | 
			
		||||
        # Use a host CPU model so that any CPU features enabled for
 | 
			
		||||
        # vulnerability mitigation are enabled.
 | 
			
		||||
        'config.nova.cpu-mode': 'host-model',
 | 
			
		||||
        # Do not override cpu-models by default.
 | 
			
		||||
        'config.nova.cpu-models': '',
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
[DEFAULT]
 | 
			
		||||
compute_driver = libvirt.LibvirtDriver
 | 
			
		||||
 | 
			
		||||
[workarounds]
 | 
			
		||||
disable_rootwrap = True
 | 
			
		||||
 | 
			
		||||
[libvirt]
 | 
			
		||||
virt_type = kvm
 | 
			
		||||
cpu_mode = host-passthrough
 | 
			
		||||
@@ -52,8 +52,9 @@ setup:
 | 
			
		||||
    horizon-snap.conf.j2: "{snap_common}/etc/horizon/horizon.conf.d/horizon-snap.conf"
 | 
			
		||||
    horizon-nginx.conf.j2: "{snap_common}/etc/nginx/snap/sites-enabled/horizon.conf"
 | 
			
		||||
    05_snap_tweaks.j2: "{snap_common}/etc/horizon/local_settings.d/_05_snap_tweaks.py"
 | 
			
		||||
    libvirtd.conf.j2: "{snap_common}/libvirt/libvirtd.conf"
 | 
			
		||||
    virtlogd.conf.j2: "{snap_common}/libvirt/virtlogd.conf"
 | 
			
		||||
    libvirtd.conf.j2: "{snap_common}/etc/libvirt/libvirtd.conf"
 | 
			
		||||
    qemu.conf.j2: "{snap_common}/etc/libvirt/qemu.conf"
 | 
			
		||||
    virtlogd.conf.j2: "{snap_common}/etc/libvirt/virtlogd.conf"
 | 
			
		||||
    microstack.rc.j2: "{snap_common}/etc/microstack.rc"
 | 
			
		||||
    microstack.json.j2: "{snap_common}/etc/microstack.json"
 | 
			
		||||
    glance.conf.d.keystone.conf.j2: "{snap_common}/etc/glance/glance.conf.d/keystone.conf"
 | 
			
		||||
@@ -118,6 +119,9 @@ setup:
 | 
			
		||||
    ovn_metadata_proxy_shared_secret: 'config.credentials.ovn-metadata-proxy-shared-secret'
 | 
			
		||||
    setup_loop_based_cinder_lvm_backend: 'config.cinder.setup-loop-based-cinder-lvm-backend'
 | 
			
		||||
    lvm_backend_volume_group: 'config.cinder.lvm-backend-volume-group'
 | 
			
		||||
    virt_type: 'config.nova.virt-type'
 | 
			
		||||
    cpu_mode: 'config.nova.cpu-mode'
 | 
			
		||||
    cpu_models: 'config.nova.cpu-models'
 | 
			
		||||
entry_points:
 | 
			
		||||
  keystone-manage:
 | 
			
		||||
    binary: "{snap}/bin/keystone-manage"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,7 @@
 | 
			
		||||
[DEFAULT]
 | 
			
		||||
 | 
			
		||||
compute_driver = libvirt.LibvirtDriver
 | 
			
		||||
 | 
			
		||||
# Set state path to writable directory
 | 
			
		||||
state_path = {{ snap_common }}/lib
 | 
			
		||||
# Log to systemd journal
 | 
			
		||||
@@ -9,6 +12,16 @@ use_journal = True
 | 
			
		||||
host = {{ node_fqdn }}
 | 
			
		||||
my_ip = {{ compute_ip }}
 | 
			
		||||
 | 
			
		||||
[workarounds]
 | 
			
		||||
disable_rootwrap = True
 | 
			
		||||
 | 
			
		||||
[libvirt]
 | 
			
		||||
virt_type = {{ virt_type }}
 | 
			
		||||
cpu_mode = {{ cpu_mode }}
 | 
			
		||||
{% if cpu_mode == 'custom' %}
 | 
			
		||||
cpu_models = {{ cpu_models }}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
[oslo_concurrency]
 | 
			
		||||
# Oslo Concurrency lock path
 | 
			
		||||
lock_path = {{ snap_common }}/lock
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								snap-overlay/templates/qemu.conf.j2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								snap-overlay/templates/qemu.conf.j2
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
# Whether libvirt should remember and restore the original
 | 
			
		||||
# ownership over files it is relabeling. Defaults to 1, set
 | 
			
		||||
# to 0 to disable the feature.
 | 
			
		||||
remember_owner = 0
 | 
			
		||||
@@ -882,7 +882,7 @@ parts:
 | 
			
		||||
      - -*
 | 
			
		||||
 | 
			
		||||
  openvswitch:
 | 
			
		||||
    source: https://github.com/openvswitch/ovs/archive/v2.13.0.tar.gz
 | 
			
		||||
    source: https://github.com/openvswitch/ovs/archive/v2.14.0.tar.gz
 | 
			
		||||
    plugin: autotools
 | 
			
		||||
    build-packages:
 | 
			
		||||
      - autoconf
 | 
			
		||||
@@ -1231,7 +1231,7 @@ parts:
 | 
			
		||||
    - --includedir=/snap/$SNAPCRAFT_PROJECT_NAME/current/usr/include
 | 
			
		||||
    - --oldincludedir=/snap/$SNAPCRAFT_PROJECT_NAME/current/usr/include
 | 
			
		||||
    - --localstatedir=/var/snap/$SNAPCRAFT_PROJECT_NAME/common
 | 
			
		||||
    - --sysconfdir=/var/snap/$SNAPCRAFT_PROJECT_NAME/common
 | 
			
		||||
    - --sysconfdir=/var/snap/$SNAPCRAFT_PROJECT_NAME/common/etc/
 | 
			
		||||
    - DNSMASQ=/snap/$SNAPCRAFT_PROJECT_NAME/current/usr/sbin/dnsmasq
 | 
			
		||||
    - DMIDECODE=/snap/$SNAPCRAFT_PROJECT_NAME/current/usr/sbin/dmidecode
 | 
			
		||||
    - OVSVSCTL=/snap/$SNAPCRAFT_PROJECT_NAME/current/usr/local/bin/ovs-vsctl
 | 
			
		||||
 
 | 
			
		||||
@@ -3,3 +3,5 @@ petname
 | 
			
		||||
selenium
 | 
			
		||||
stestr
 | 
			
		||||
xvfbwrapper
 | 
			
		||||
netifaces
 | 
			
		||||
tenacity
 | 
			
		||||
 
 | 
			
		||||
@@ -1,55 +1,24 @@
 | 
			
		||||
import logging
 | 
			
		||||
import json
 | 
			
		||||
import unittest
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
import time
 | 
			
		||||
from typing import List
 | 
			
		||||
import yaml
 | 
			
		||||
 | 
			
		||||
import petname
 | 
			
		||||
import tenacity
 | 
			
		||||
from selenium import webdriver
 | 
			
		||||
from selenium.webdriver.firefox.options import Options as FirefoxOptions
 | 
			
		||||
from selenium.webdriver.common.by import By
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Setup logging
 | 
			
		||||
log = logging.getLogger("microstack_test")
 | 
			
		||||
log.setLevel(logging.DEBUG)
 | 
			
		||||
logger = logging.getLogger("microstack_test")
 | 
			
		||||
logger.setLevel(logging.DEBUG)
 | 
			
		||||
stream = logging.StreamHandler()
 | 
			
		||||
formatter = logging.Formatter(
 | 
			
		||||
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 | 
			
		||||
stream.setFormatter(formatter)
 | 
			
		||||
log.addHandler(stream)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check(*args: List[str]) -> int:
 | 
			
		||||
    """Execute a shell command, raising an error on failed excution.
 | 
			
		||||
 | 
			
		||||
    :param args: strings to be composed into the bash call.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    return subprocess.check_call(args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_output(*args: List[str]) -> str:
 | 
			
		||||
    """Execute a shell command, returning the output of the command.
 | 
			
		||||
 | 
			
		||||
    :param args: strings to be composed into the bash call.
 | 
			
		||||
 | 
			
		||||
    Include our env; pass in any extra keyword args.
 | 
			
		||||
    """
 | 
			
		||||
    return subprocess.check_output(args, universal_newlines=True).strip()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def call(*args: List[str]) -> bool:
 | 
			
		||||
    """Execute a shell command.
 | 
			
		||||
 | 
			
		||||
    Return True if the call executed successfully (returned 0), or
 | 
			
		||||
    False if it returned with an error code (return > 0)
 | 
			
		||||
 | 
			
		||||
    :param args: strings to be composed into the bash call.
 | 
			
		||||
    """
 | 
			
		||||
    return not subprocess.call(args)
 | 
			
		||||
logger.addHandler(stream)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def gui_wrapper(func):
 | 
			
		||||
@@ -73,135 +42,351 @@ def gui_wrapper(func):
 | 
			
		||||
    return wrapper
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Host():
 | 
			
		||||
    """A host with MicroStack installed."""
 | 
			
		||||
class TestHost:
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.prefix = []
 | 
			
		||||
        self.dump_dir = '/tmp'
 | 
			
		||||
        self.machine = ''
 | 
			
		||||
        self.distro = os.environ.get('DISTRO') or 'bionic'
 | 
			
		||||
        self.snap = os.environ.get('SNAP_FILE') or \
 | 
			
		||||
            'microstack_ussuri_amd64.snap'
 | 
			
		||||
        self.horizon_ip = '10.20.20.1'
 | 
			
		||||
        self.host_type = 'localhost'
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
        if os.environ.get('MULTIPASS'):
 | 
			
		||||
            self.host_type = 'multipass'
 | 
			
		||||
            print("Booting a Multipass VM ...")
 | 
			
		||||
            self.multipass()
 | 
			
		||||
    def destroy(self):
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
        self.microstack_test()
 | 
			
		||||
    def check_output(self, args, **kwargs):
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def install(self, snap=None, channel='dangerous'):
 | 
			
		||||
        if snap is None:
 | 
			
		||||
            snap = self.snap
 | 
			
		||||
        print("Installing {}".format(snap))
 | 
			
		||||
    def call(self, args, **kwargs):
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
        check(*self.prefix, 'sudo', 'snap', 'install',
 | 
			
		||||
              '--{}'.format(channel), '--devmode', snap)
 | 
			
		||||
    def check_call(self, args, **kwargs):
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def install_snap(self, name, options):
 | 
			
		||||
        self.check_output(['sudo', 'snap', 'install', name, *options])
 | 
			
		||||
 | 
			
		||||
    def remove_snap(self, name, options):
 | 
			
		||||
        self.check_output(['sudo', 'snap', 'remove', name, *options])
 | 
			
		||||
 | 
			
		||||
    def snap_connect(self, snap_name, plug_name):
 | 
			
		||||
        self.check_output(['sudo', 'snap', 'connect',
 | 
			
		||||
                          f'{snap_name}:{plug_name}'])
 | 
			
		||||
 | 
			
		||||
    def install_microstack(self, *, channel='edge', path=None):
 | 
			
		||||
        """Install MicroStack at this host and connect relevant plugs.
 | 
			
		||||
        """
 | 
			
		||||
        if path is not None:
 | 
			
		||||
            self.install_snap(path, ['--devmode'])
 | 
			
		||||
        else:
 | 
			
		||||
            self.install_snap('microstack', [f'--{channel}', '--devmode'])
 | 
			
		||||
 | 
			
		||||
        # TODO: add microstack-support once it is merged into snapd.
 | 
			
		||||
        connections = [
 | 
			
		||||
                'microstack:libvirt', 'microstack:netlink-audit',
 | 
			
		||||
                'microstack:firewall-control', 'microstack:hardware-observe',
 | 
			
		||||
                'microstack:kernel-module-observe', 'microstack:kvm',
 | 
			
		||||
                'microstack:log-observe', 'microstack:mount-observe',
 | 
			
		||||
                'microstack:netlink-connector', 'microstack:network-observe',
 | 
			
		||||
                'microstack:openvswitch-support', 'microstack:process-control',
 | 
			
		||||
                'microstack:system-observe', 'microstack:network-control',
 | 
			
		||||
                'microstack:system-trace', 'microstack:block-devices',
 | 
			
		||||
                'microstack:raw-usb'
 | 
			
		||||
        plugs = [
 | 
			
		||||
                'libvirt', 'netlink-audit',
 | 
			
		||||
                'firewall-control', 'hardware-observe',
 | 
			
		||||
                'kernel-module-observe', 'kvm',
 | 
			
		||||
                'log-observe', 'mount-observe',
 | 
			
		||||
                'netlink-connector', 'network-observe',
 | 
			
		||||
                'openvswitch-support', 'process-control',
 | 
			
		||||
                'system-observe', 'network-control',
 | 
			
		||||
                'system-trace', 'block-devices',
 | 
			
		||||
                'raw-usb'
 | 
			
		||||
        ]
 | 
			
		||||
        for connection in connections:
 | 
			
		||||
            check('sudo', 'snap', 'connect', connection)
 | 
			
		||||
        for plug in plugs:
 | 
			
		||||
            self.snap_connect('microstack', plug)
 | 
			
		||||
 | 
			
		||||
    def init(self, args=['--auto']):
 | 
			
		||||
        print(f"Initializing the snap with {args}")
 | 
			
		||||
        check(*self.prefix, 'sudo', 'microstack', 'init', *args)
 | 
			
		||||
    def init_microstack(self, args=['--auto']):
 | 
			
		||||
        self.check_call(['sudo', 'microstack', 'init', *args])
 | 
			
		||||
 | 
			
		||||
    def multipass(self):
 | 
			
		||||
        self.machine = petname.generate()
 | 
			
		||||
        self.prefix = ['multipass', 'exec', self.machine, '--']
 | 
			
		||||
    def setup_tempest_verifier(self):
 | 
			
		||||
        self.check_call(['sudo', 'snap', 'install', 'microstack-test'])
 | 
			
		||||
        self.check_call(['sudo', 'mkdir', '-p',
 | 
			
		||||
                        '/tmp/snap.microstack-test/tmp'])
 | 
			
		||||
        self.check_call(['sudo', 'cp',
 | 
			
		||||
                         '/var/snap/microstack/common/etc/microstack.json',
 | 
			
		||||
                         '/tmp/snap.microstack-test/tmp/microstack.json'])
 | 
			
		||||
        self.check_call(['microstack-test.rally', 'db', 'recreate'])
 | 
			
		||||
        self.check_call([
 | 
			
		||||
            'microstack-test.rally', 'deployment', 'create',
 | 
			
		||||
            '--filename', '/tmp/microstack.json',
 | 
			
		||||
            '--name', 'snap_generated'])
 | 
			
		||||
        self.check_call(['microstack-test.tempest-init'])
 | 
			
		||||
 | 
			
		||||
        check('sudo', 'snap', 'install', '--classic', '--edge', 'multipass')
 | 
			
		||||
    def run_verifications(self):
 | 
			
		||||
        """Run a set of verification tests on MicroStack from this host."""
 | 
			
		||||
        self.check_call([
 | 
			
		||||
            'microstack-test.rally', 'verify', 'start',
 | 
			
		||||
            '--load-list',
 | 
			
		||||
            '/snap/microstack-test/current/2020.06-test-list.txt',
 | 
			
		||||
            '--detailed', '--concurrency', '2'])
 | 
			
		||||
        self.check_call([
 | 
			
		||||
            'microstack-test.rally', 'verify', 'report',
 | 
			
		||||
            '--type', 'json', '--to',
 | 
			
		||||
            '/tmp/verification-report.json'])
 | 
			
		||||
        report = json.loads(self.check_output([
 | 
			
		||||
            'sudo', 'cat',
 | 
			
		||||
            '/tmp/snap.microstack-test/tmp/verification-report.json']))
 | 
			
		||||
        # Make sure there are no verification failures in the report.
 | 
			
		||||
        failures = list(report['verifications'].values())[0]['failures']
 | 
			
		||||
        return failures
 | 
			
		||||
 | 
			
		||||
        check('multipass', 'launch', '--cpus', '2', '--mem', '8G', self.distro,
 | 
			
		||||
              '--name', self.machine)
 | 
			
		||||
        check('multipass', 'copy-files', self.snap, '{}:'.format(self.machine))
 | 
			
		||||
 | 
			
		||||
        # Figure out machine's ip
 | 
			
		||||
        info = check_output('multipass', 'info', self.machine, '--format',
 | 
			
		||||
                            'json')
 | 
			
		||||
        info = json.loads(info)
 | 
			
		||||
        self.horizon_ip = info['info'][self.machine]['ipv4'][0]
 | 
			
		||||
class LocalTestHost(TestHost):
 | 
			
		||||
 | 
			
		||||
    def microstack_test(self):
 | 
			
		||||
        check('sudo', 'snap', 'install', 'microstack-test')
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.install_snap('multipass', ['--stable'])
 | 
			
		||||
        self.install_snap('lxd', ['--stable'])
 | 
			
		||||
        self.check_call(['sudo', 'lxd', 'init', '--auto'])
 | 
			
		||||
 | 
			
		||||
    def dump_logs(self):
 | 
			
		||||
        # TODO: make unique log name
 | 
			
		||||
        if check_output('whoami') == 'zuul':
 | 
			
		||||
            self.dump_dir = "/home/zuul/zuul-output/logs"
 | 
			
		||||
        try:
 | 
			
		||||
            self.run(['sudo', 'lxc', 'profile', 'show', 'microstack'],
 | 
			
		||||
                     stdout=subprocess.PIPE, stderr=subprocess.PIPE,
 | 
			
		||||
                     check=True)
 | 
			
		||||
        except subprocess.CalledProcessError as e:
 | 
			
		||||
            stderr = e.stderr.decode('utf-8')
 | 
			
		||||
            if 'No such object' in stderr:
 | 
			
		||||
                self._create_microstack_profile()
 | 
			
		||||
                return
 | 
			
		||||
            else:
 | 
			
		||||
                raise RuntimeError(
 | 
			
		||||
                    'An unexpected exception has occurred '
 | 
			
		||||
                    f'while trying to query the profile, stderr: {stderr}'
 | 
			
		||||
                ) from e
 | 
			
		||||
        self.run(['sudo', 'lxc', 'profile', 'delete', 'microstack'],
 | 
			
		||||
                 check=True)
 | 
			
		||||
        self._create_microstack_profile()
 | 
			
		||||
 | 
			
		||||
        check(*self.prefix,
 | 
			
		||||
              'sudo', 'tar', 'cvzf',
 | 
			
		||||
              '{}/dump.tar.gz'.format(self.dump_dir),
 | 
			
		||||
              '/var/snap/microstack/common/log',
 | 
			
		||||
              '/var/snap/microstack/common/etc',
 | 
			
		||||
              '/var/log/syslog')
 | 
			
		||||
        if 'multipass' in self.prefix:
 | 
			
		||||
            check('multipass', 'copy-files',
 | 
			
		||||
                  '{}:/tmp/dump.tar.gz'.format(self.machine), '.')
 | 
			
		||||
        print('Saved dump.tar.gz to local working dir.')
 | 
			
		||||
    def _create_microstack_profile(self):
 | 
			
		||||
        self.run(['sudo', 'lxc', 'profile', 'create', 'microstack'],
 | 
			
		||||
                 stdout=subprocess.PIPE,
 | 
			
		||||
                 stderr=subprocess.PIPE,
 | 
			
		||||
                 check=True)
 | 
			
		||||
        profile_conf = {
 | 
			
		||||
            'config': {'linux.kernel_modules':
 | 
			
		||||
                       'iptable_nat, ip6table_nat, ebtables, openvswitch,'
 | 
			
		||||
                       'tap, vhost, vhost_net, vhost_scsi, vhost_vsock',
 | 
			
		||||
                       'security.nesting': 'true',
 | 
			
		||||
                       'limits.kernel.memlock': 'unlimited'
 | 
			
		||||
                       },
 | 
			
		||||
            'devices':
 | 
			
		||||
            {
 | 
			
		||||
                'tun': {'path': '/dev/net/tun', 'type': 'unix-char'},
 | 
			
		||||
                'vhost-net': {'path': '/dev/vhost-net', 'type': 'unix-char'},
 | 
			
		||||
                'vhost-scsi': {'path': '/dev/vhost-scsi', 'type': 'unix-char'},
 | 
			
		||||
                'vhost-vsock': {'path': '/dev/vhost-vsock',
 | 
			
		||||
                                'type': 'unix-char'}
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        self.run(['sudo', 'lxc', 'profile', 'edit', 'microstack'],
 | 
			
		||||
                 stdout=subprocess.PIPE,
 | 
			
		||||
                 stderr=subprocess.PIPE,
 | 
			
		||||
                 check=True,
 | 
			
		||||
                 input=yaml.dump(profile_conf).encode('utf-8'))
 | 
			
		||||
 | 
			
		||||
    def teardown(self):
 | 
			
		||||
        if 'multipass' in self.prefix:
 | 
			
		||||
            check('sudo', 'multipass', 'delete', self.machine)
 | 
			
		||||
            check('sudo', 'multipass', 'purge')
 | 
			
		||||
        else:
 | 
			
		||||
            if call('snap', 'list', 'microstack'):
 | 
			
		||||
                # Uninstall microstack if it is installed (it may not be).
 | 
			
		||||
                check('sudo', 'snap', 'remove', '--purge', 'microstack')
 | 
			
		||||
    def destroy(self):
 | 
			
		||||
        self.remove_snap('microstack', ['--purge'])
 | 
			
		||||
 | 
			
		||||
    def check_output(self, args, **kwargs):
 | 
			
		||||
        return subprocess.check_output(args, **kwargs).strip()
 | 
			
		||||
 | 
			
		||||
    def call(self, args, **kwargs):
 | 
			
		||||
        return subprocess.call(args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def check_call(self, args, **kwargs):
 | 
			
		||||
        subprocess.check_call(args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def run(self, args, **kwargs):
 | 
			
		||||
        subprocess.run(args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MultipassTestHost(TestHost):
 | 
			
		||||
    """A virtual host set up via Multipass on a local machine."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, distribution):
 | 
			
		||||
        self.distribution = distribution
 | 
			
		||||
        self.name = petname.generate()
 | 
			
		||||
        self._launch()
 | 
			
		||||
 | 
			
		||||
    def check_output(self, args, **kwargs):
 | 
			
		||||
        prefix = ['sudo', 'multipass', 'exec', self.name, '--']
 | 
			
		||||
        cmd = []
 | 
			
		||||
        cmd.extend(prefix)
 | 
			
		||||
        cmd.extend(args)
 | 
			
		||||
        return subprocess.check_output(cmd, **kwargs).strip()
 | 
			
		||||
 | 
			
		||||
    def call(self, args, **kwargs):
 | 
			
		||||
        prefix = ['sudo', 'multipass', 'exec', self.name, '--']
 | 
			
		||||
        cmd = []
 | 
			
		||||
        cmd.extend(prefix)
 | 
			
		||||
        cmd.extend(args)
 | 
			
		||||
        return subprocess.call(cmd, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def check_call(self, args, **kwargs):
 | 
			
		||||
        prefix = ['sudo', 'multipass', 'exec', self.name, '--']
 | 
			
		||||
        cmd = []
 | 
			
		||||
        cmd.extend(prefix)
 | 
			
		||||
        cmd.extend(args)
 | 
			
		||||
        subprocess.check_call(cmd, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def run(self, args, **kwargs):
 | 
			
		||||
        prefix = ['sudo', 'multipass', 'exec', self.name, '--']
 | 
			
		||||
        cmd = []
 | 
			
		||||
        cmd.extend(prefix)
 | 
			
		||||
        cmd.extend(args)
 | 
			
		||||
        subprocess.run(cmd, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def _launch(self):
 | 
			
		||||
        # Possible upstream CI resource allocation is documented here:
 | 
			
		||||
        # https://docs.opendev.org/opendev/infra-manual/latest/testing.html
 | 
			
		||||
        # >= 8GiB of RAM
 | 
			
		||||
        # 40-80 GB of storage (possibly under /opt)
 | 
			
		||||
        # Swap is not guaranteed.
 | 
			
		||||
        # With m1.tiny flavor the compute node needs slightly less than 3G of
 | 
			
		||||
        # RAM and 2.5G of disk space.
 | 
			
		||||
        subprocess.check_call(['sudo', 'sync'])
 | 
			
		||||
        subprocess.check_call(['sudo', 'sh', '-c',
 | 
			
		||||
                               'echo 3 > /proc/sys/vm/drop_caches'])
 | 
			
		||||
        subprocess.check_call(['sudo', 'multipass', 'launch', '--cpus', '2',
 | 
			
		||||
                               '--mem', '3G', '--disk', '4G',
 | 
			
		||||
                               self.distribution, '--name', self.name])
 | 
			
		||||
 | 
			
		||||
        info = json.loads(subprocess.check_output(
 | 
			
		||||
            ['sudo', 'multipass', 'info', self.name,
 | 
			
		||||
             '--format', 'json']))
 | 
			
		||||
        self.address = info['info'][self.name]['ipv4'][0]
 | 
			
		||||
 | 
			
		||||
    def copy_to(self, source_path, target_path=''):
 | 
			
		||||
        """Copy a file from the local machine to the Multipass VM.
 | 
			
		||||
        """
 | 
			
		||||
        subprocess.check_call(['sudo', 'multipass', 'copy-files', source_path,
 | 
			
		||||
                               f'{self.name}:{target_path}'])
 | 
			
		||||
 | 
			
		||||
    def copy_from(self, source_path, target_path):
 | 
			
		||||
        """Copy a file from the Multipass VM to the local machine.
 | 
			
		||||
        """
 | 
			
		||||
        subprocess.check_call(['sudo', 'multipass', 'copy-files',
 | 
			
		||||
                               f'{self.name}:{source_path}',
 | 
			
		||||
                               target_path])
 | 
			
		||||
 | 
			
		||||
    def destroy(self):
 | 
			
		||||
        subprocess.check_call(['sudo', 'multipass', 'delete', self.name])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LXDTestHost(TestHost):
 | 
			
		||||
    """A container test host set up via LXD on a local machine."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, distribution):
 | 
			
		||||
        self.distribution = distribution
 | 
			
		||||
        self.name = petname.generate()
 | 
			
		||||
        self._launch()
 | 
			
		||||
 | 
			
		||||
    def check_output(self, args, **kwargs):
 | 
			
		||||
        prefix = ['sudo', 'lxc', 'exec', self.name, '--']
 | 
			
		||||
        cmd = []
 | 
			
		||||
        cmd.extend(prefix)
 | 
			
		||||
        cmd.extend(args)
 | 
			
		||||
        return subprocess.check_output(cmd, **kwargs).strip()
 | 
			
		||||
 | 
			
		||||
    def call(self, args, **kwargs):
 | 
			
		||||
        prefix = ['sudo', 'lxc', 'exec', self.name, '--']
 | 
			
		||||
        cmd = []
 | 
			
		||||
        cmd.extend(prefix)
 | 
			
		||||
        cmd.extend(args)
 | 
			
		||||
        return subprocess.call(cmd, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def check_call(self, args, **kwargs):
 | 
			
		||||
        prefix = ['sudo', 'lxc', 'exec', self.name, '--']
 | 
			
		||||
        cmd = []
 | 
			
		||||
        cmd.extend(prefix)
 | 
			
		||||
        cmd.extend(args)
 | 
			
		||||
        subprocess.check_call(cmd, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def run(self, args, **kwargs):
 | 
			
		||||
        prefix = ['sudo', 'lxc', 'exec', self.name, '--']
 | 
			
		||||
        cmd = []
 | 
			
		||||
        cmd.extend(prefix)
 | 
			
		||||
        cmd.extend(args)
 | 
			
		||||
        subprocess.check_call(cmd, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def _launch(self):
 | 
			
		||||
        subprocess.check_call(['sudo', 'lxc', 'launch',
 | 
			
		||||
                               f'ubuntu:{self.distribution}', self.name,
 | 
			
		||||
                               '--profile', 'default',
 | 
			
		||||
                               '--profile', 'microstack'])
 | 
			
		||||
 | 
			
		||||
        @tenacity.retry(wait=tenacity.wait_fixed(3))
 | 
			
		||||
        def fetch_addr_info():
 | 
			
		||||
            info = json.loads(subprocess.check_output(
 | 
			
		||||
                ['sudo', 'lxc', 'query', f'/1.0/instances/{self.name}/state']))
 | 
			
		||||
            addrs = info['network']['eth0']['addresses']
 | 
			
		||||
            addr_info = next(filter(lambda a: a['family'] == 'inet', addrs),
 | 
			
		||||
                             None)
 | 
			
		||||
            if addr_info is None:
 | 
			
		||||
                raise RuntimeError('The container interface does'
 | 
			
		||||
                                   ' not have an IPv4 address which'
 | 
			
		||||
                                   ' is unexpected')
 | 
			
		||||
            return addr_info
 | 
			
		||||
        self.address = fetch_addr_info()['address']
 | 
			
		||||
 | 
			
		||||
    def copy_to(self, source_path, target_path=''):
 | 
			
		||||
        """Copy file or directory to the container.
 | 
			
		||||
        """
 | 
			
		||||
        subprocess.check_call(['sudo', 'lxc', 'file', 'push', source_path,
 | 
			
		||||
                               f'{self.name}/{target_path}',
 | 
			
		||||
                               '--recursive', '--create-dirs'])
 | 
			
		||||
 | 
			
		||||
    def copy_from(self, source_path, target_path):
 | 
			
		||||
        """Copy file or directory from the container.
 | 
			
		||||
        """
 | 
			
		||||
        subprocess.check_call(['sudo', 'lxc', 'file', 'pull'
 | 
			
		||||
                               f'{self.name}/{source_path}', target_path,
 | 
			
		||||
                               '--recursive', '--create-dirs'])
 | 
			
		||||
 | 
			
		||||
    def destroy(self):
 | 
			
		||||
        subprocess.check_call(['sudo', 'lxc', 'delete', self.name, '--force'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Framework(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.HOSTS = []
 | 
			
		||||
        self._test_hosts = []
 | 
			
		||||
 | 
			
		||||
    def get_host(self):
 | 
			
		||||
        if self.HOSTS:
 | 
			
		||||
            return self.HOSTS[0]
 | 
			
		||||
        host = Host()
 | 
			
		||||
        self.HOSTS.append(host)
 | 
			
		||||
        return host
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self._localhost = LocalTestHost()
 | 
			
		||||
 | 
			
		||||
    def add_host(self):
 | 
			
		||||
        host = Host()
 | 
			
		||||
        self.HOSTS.append(host)
 | 
			
		||||
        return host
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        for host in self._test_hosts:
 | 
			
		||||
            host.destroy()
 | 
			
		||||
 | 
			
		||||
    def verify_instance_networking(self, host, instance_name):
 | 
			
		||||
    @property
 | 
			
		||||
    def localhost(self):
 | 
			
		||||
        return self._localhost
 | 
			
		||||
 | 
			
		||||
    def add_multipass_host(self, distribution):
 | 
			
		||||
        new_test_host = MultipassTestHost(distribution)
 | 
			
		||||
        self._test_hosts.append(new_test_host)
 | 
			
		||||
        return new_test_host
 | 
			
		||||
 | 
			
		||||
    def add_lxd_host(self, distribution):
 | 
			
		||||
        new_test_host = LXDTestHost(distribution)
 | 
			
		||||
        self._test_hosts.append(new_test_host)
 | 
			
		||||
        return new_test_host
 | 
			
		||||
 | 
			
		||||
    def verify_instance_networking(self, test_host, instance_name):
 | 
			
		||||
        """Verify that we have networking on an instance
 | 
			
		||||
 | 
			
		||||
        We should be able to ping the instance.
 | 
			
		||||
 | 
			
		||||
        And we should be able to reach the Internet.
 | 
			
		||||
 | 
			
		||||
        :param :class:`TestHost` test_host: The host to run the test from.
 | 
			
		||||
        :param str instance_name: The name of the Nova instance to connect to.
 | 
			
		||||
        """
 | 
			
		||||
        print("Skipping instance networking test due to bug #1852206")
 | 
			
		||||
        # TODO re-enable this test when we have fixed
 | 
			
		||||
        # https://bugs.launchpad.net/microstack/+bug/1852206
 | 
			
		||||
        return True
 | 
			
		||||
        prefix = host.prefix
 | 
			
		||||
 | 
			
		||||
        # Ping the instance
 | 
			
		||||
        print("Testing ping ...")
 | 
			
		||||
        logger.debug("Testing ping ...")
 | 
			
		||||
        ip = None
 | 
			
		||||
        servers = check_output(*prefix, '/snap/bin/microstack.openstack',
 | 
			
		||||
                               'server', 'list', '--format', 'json')
 | 
			
		||||
        servers = test_host.check_output([
 | 
			
		||||
            '/snap/bin/microstack.openstack',
 | 
			
		||||
            'server', 'list', '--format', 'json'
 | 
			
		||||
        ])
 | 
			
		||||
        servers = json.loads(servers)
 | 
			
		||||
        for server in servers:
 | 
			
		||||
            if server['Name'] == instance_name:
 | 
			
		||||
@@ -210,56 +395,25 @@ class Framework(unittest.TestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(ip)
 | 
			
		||||
 | 
			
		||||
        pings = 1
 | 
			
		||||
        max_pings = 600  # ~10 minutes!
 | 
			
		||||
        while not call(*prefix, 'ping', '-c1', '-w1', ip):
 | 
			
		||||
            pings += 1
 | 
			
		||||
            if pings > max_pings:
 | 
			
		||||
                self.assertFalse(True, msg='Max pings reached!')
 | 
			
		||||
 | 
			
		||||
        print("Testing instances' ability to connect to the Internet")
 | 
			
		||||
        # Test Internet connectivity
 | 
			
		||||
        attempts = 1
 | 
			
		||||
        max_attempts = 300  # ~10 minutes!
 | 
			
		||||
        username = check_output(*prefix, 'whoami')
 | 
			
		||||
 | 
			
		||||
        while not call(
 | 
			
		||||
                *prefix,
 | 
			
		||||
                'ssh',
 | 
			
		||||
                '-oStrictHostKeyChecking=no',
 | 
			
		||||
                '-i', '/home/{}/.ssh/id_microstack'.format(username),
 | 
			
		||||
                'cirros@{}'.format(ip),
 | 
			
		||||
                '--', 'ping', '-c1', '91.189.94.250'):
 | 
			
		||||
            attempts += 1
 | 
			
		||||
            if attempts > max_attempts:
 | 
			
		||||
                self.assertFalse(
 | 
			
		||||
                    True,
 | 
			
		||||
                    msg='Unable to access the Internet!')
 | 
			
		||||
            time.sleep(1)
 | 
			
		||||
        test_host.call(['ping', '-i1', '-c10', '-w11', ip])
 | 
			
		||||
 | 
			
		||||
    @gui_wrapper
 | 
			
		||||
    def verify_gui(self, host):
 | 
			
		||||
        """Verify that Horizon Dashboard works
 | 
			
		||||
 | 
			
		||||
        We should be able to reach the dashboard.
 | 
			
		||||
 | 
			
		||||
        We should be able to login.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        # Test
 | 
			
		||||
        print('Verifying GUI for (IP: {})'.format(host.horizon_ip))
 | 
			
		||||
        # Verify that our GUI is working properly
 | 
			
		||||
        dashboard_port = check_output(*host.prefix, 'sudo', 'snap', 'get',
 | 
			
		||||
                                      'microstack',
 | 
			
		||||
                                      'config.network.ports.dashboard')
 | 
			
		||||
        keystone_password = check_output(
 | 
			
		||||
            *host.prefix, 'sudo', 'snap', 'get',
 | 
			
		||||
    def verify_gui(self, test_host):
 | 
			
		||||
        """Verify Horizon Dashboard operation by logging in."""
 | 
			
		||||
        control_ip = test_host.check_output([
 | 
			
		||||
            'sudo', 'snap', 'get', 'microstack', 'config.network.control-ip',
 | 
			
		||||
        ]).decode('utf-8')
 | 
			
		||||
        logger.debug('Verifying GUI for (IP: {})'.format(control_ip))
 | 
			
		||||
        dashboard_port = test_host.check_output([
 | 
			
		||||
            'sudo', 'snap', 'get',
 | 
			
		||||
            'microstack',
 | 
			
		||||
            'config.credentials.keystone-password')
 | 
			
		||||
        self.driver.get("http://{}:{}/".format(
 | 
			
		||||
            host.horizon_ip,
 | 
			
		||||
            dashboard_port
 | 
			
		||||
        ))
 | 
			
		||||
            'config.network.ports.dashboard']).decode('utf-8')
 | 
			
		||||
        keystone_password = test_host.check_output([
 | 
			
		||||
            'sudo', 'snap', 'get',
 | 
			
		||||
            'microstack',
 | 
			
		||||
            'config.credentials.keystone-password'
 | 
			
		||||
        ]).decode('utf-8')
 | 
			
		||||
        self.driver.get(f'http://{control_ip}:{dashboard_port}/')
 | 
			
		||||
        # Login to horizon!
 | 
			
		||||
        self.driver.find_element(By.ID, "id_username").click()
 | 
			
		||||
        self.driver.find_element(By.ID, "id_username").send_keys("admin")
 | 
			
		||||
@@ -269,26 +423,3 @@ class Framework(unittest.TestCase):
 | 
			
		||||
        # Verify that we can click something on the dashboard -- e.g.,
 | 
			
		||||
        # we're still not sitting at the login screen.
 | 
			
		||||
        self.driver.find_element(By.LINK_TEXT, "Images").click()
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.passed = False  # HACK: trigger (or skip) log dumps.
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        """Clean hosts up, possibly leaving debug information behind."""
 | 
			
		||||
 | 
			
		||||
        print("Tests complete. Cleaning up.")
 | 
			
		||||
        while self.HOSTS:
 | 
			
		||||
            host = self.HOSTS.pop()
 | 
			
		||||
            if not self.passed:
 | 
			
		||||
                print(
 | 
			
		||||
                    "Tests failed. Leaving {} in place.".format(host.machine))
 | 
			
		||||
                # Skipping log dump, due to
 | 
			
		||||
                # https://bugs.launchpad.net/microstack/+bug/1860783
 | 
			
		||||
                # host.dump_logs()
 | 
			
		||||
                if os.environ.get('INTERACTIVE_DEBUG'):
 | 
			
		||||
                    print('INTERACTIVE_DEBUG set. '
 | 
			
		||||
                          'Opening a shell on test machine.')
 | 
			
		||||
                    call('multipass', 'shell', host.machine)
 | 
			
		||||
            else:
 | 
			
		||||
                print("Tests complete. Cleaning up.")
 | 
			
		||||
                host.teardown()
 | 
			
		||||
 
 | 
			
		||||
@@ -16,13 +16,11 @@ Web IDE.
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
import time
 | 
			
		||||
import json
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
sys.path.append(os.getcwd())
 | 
			
		||||
 | 
			
		||||
from tests.framework import Framework, check, check_output, call  # noqa E402
 | 
			
		||||
from tests.framework import Framework  # noqa E402
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestBasics(Framework):
 | 
			
		||||
@@ -34,23 +32,21 @@ class TestBasics(Framework):
 | 
			
		||||
        open the Horizon GUI.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        host = self.get_host()
 | 
			
		||||
        host.install()
 | 
			
		||||
        host.init([
 | 
			
		||||
        self._localhost.install_microstack(path='microstack_ussuri_amd64.snap')
 | 
			
		||||
        self._localhost.init_microstack([
 | 
			
		||||
            '--auto',
 | 
			
		||||
            '--control',
 | 
			
		||||
            '--setup-loop-based-cinder-lvm-backend',
 | 
			
		||||
            '--loop-device-file-size=24'
 | 
			
		||||
            ])
 | 
			
		||||
        prefix = host.prefix
 | 
			
		||||
        endpoints = self._localhost.check_output(
 | 
			
		||||
            ['/snap/bin/microstack.openstack', 'endpoint', 'list']
 | 
			
		||||
        ).decode('utf-8')
 | 
			
		||||
 | 
			
		||||
        endpoints = check_output(
 | 
			
		||||
            *prefix, '/snap/bin/microstack.openstack', 'endpoint', 'list')
 | 
			
		||||
        control_ip = self._localhost.check_output(
 | 
			
		||||
            ['sudo', 'snap', 'get', 'microstack', 'config.network.control-ip'],
 | 
			
		||||
        ).decode('utf-8')
 | 
			
		||||
 | 
			
		||||
        control_ip = check_output(
 | 
			
		||||
            *prefix, 'sudo', 'snap', 'get',
 | 
			
		||||
            'microstack', 'config.network.control-ip'
 | 
			
		||||
        )
 | 
			
		||||
        # Endpoints should contain the control IP.
 | 
			
		||||
        self.assertTrue(control_ip in endpoints)
 | 
			
		||||
 | 
			
		||||
@@ -58,64 +54,46 @@ class TestBasics(Framework):
 | 
			
		||||
        self.assertFalse("localhost" in endpoints)
 | 
			
		||||
 | 
			
		||||
        # We should be able to launch an instance
 | 
			
		||||
        instance_name = 'test-instance'
 | 
			
		||||
        print("Testing microstack.launch ...")
 | 
			
		||||
        check(*prefix, '/snap/bin/microstack.launch', 'cirros',
 | 
			
		||||
              '--name', 'breakfast', '--retry')
 | 
			
		||||
 | 
			
		||||
        # ... and ping it
 | 
			
		||||
        # Skip these tests in the gate, as they are not reliable there.
 | 
			
		||||
        # TODO: fix these in the gate!
 | 
			
		||||
        if 'multipass' in prefix:
 | 
			
		||||
            self.verify_instance_networking(host, 'breakfast')
 | 
			
		||||
        else:
 | 
			
		||||
            # Artificial wait, to allow for stuff to settle for the GUI test.
 | 
			
		||||
            # TODO: get rid of this, when we drop the ping tests back int.
 | 
			
		||||
            time.sleep(10)
 | 
			
		||||
        self._localhost.check_output(
 | 
			
		||||
            ['/snap/bin/microstack.launch', 'cirros',
 | 
			
		||||
             '--name', instance_name, '--retry']
 | 
			
		||||
        )
 | 
			
		||||
        self.verify_instance_networking(self._localhost, instance_name)
 | 
			
		||||
 | 
			
		||||
        # The Horizon Dashboard should function
 | 
			
		||||
        self.verify_gui(host)
 | 
			
		||||
        self.verify_gui(self._localhost)
 | 
			
		||||
 | 
			
		||||
        # Verify that we can uninstall the snap cleanly, and that the
 | 
			
		||||
        # ovs bridge goes away.
 | 
			
		||||
 | 
			
		||||
        # Check to verify that our bridge is there.
 | 
			
		||||
        self.assertTrue('br-ex' in check_output(*prefix, 'ip', 'a'))
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            'br-ex' in self._localhost.check_output(
 | 
			
		||||
                ['ip', 'a']).decode('utf-8'))
 | 
			
		||||
 | 
			
		||||
        check(*prefix, 'sudo', 'mkdir', '-p', '/tmp/snap.microstack-test/tmp')
 | 
			
		||||
        check(*prefix, 'sudo', 'cp',
 | 
			
		||||
              '/var/snap/microstack/common/etc/microstack.json',
 | 
			
		||||
              '/tmp/snap.microstack-test/tmp/microstack.json')
 | 
			
		||||
        check(*prefix, 'microstack-test.rally', 'db', 'recreate')
 | 
			
		||||
        check(*prefix, 'microstack-test.rally', 'deployment', 'create',
 | 
			
		||||
              '--filename', '/tmp/microstack.json',
 | 
			
		||||
              '--name', 'snap_generated')
 | 
			
		||||
        check(*prefix, 'microstack-test.tempest-init')
 | 
			
		||||
        check(*prefix, 'microstack-test.rally', 'verify', 'start',
 | 
			
		||||
              '--load-list',
 | 
			
		||||
              '/snap/microstack-test/current/2020.06-test-list.txt',
 | 
			
		||||
              '--detailed', '--concurrency', '2')
 | 
			
		||||
        check(*prefix, 'microstack-test.rally', 'verify', 'report',
 | 
			
		||||
              '--type', 'json', '--to',
 | 
			
		||||
              '/tmp/verification-report.json')
 | 
			
		||||
        report = json.loads(check_output(
 | 
			
		||||
            *prefix, 'sudo', 'cat',
 | 
			
		||||
            '/tmp/snap.microstack-test/tmp/verification-report.json'))
 | 
			
		||||
        self._localhost.setup_tempest_verifier()
 | 
			
		||||
        # Make sure there are no verification failures in the report.
 | 
			
		||||
        failures = list(report['verifications'].values())[0]['failures']
 | 
			
		||||
        failures = self._localhost.run_verifications()
 | 
			
		||||
        self.assertEqual(failures, 0, 'Verification tests had failure.')
 | 
			
		||||
 | 
			
		||||
        # Try to remove the snap without sudo.
 | 
			
		||||
        self.assertFalse(
 | 
			
		||||
                call(*prefix, 'snap', 'remove', '--purge', 'microstack'))
 | 
			
		||||
        self.assertEqual(self._localhost.call([
 | 
			
		||||
            'snap', 'remove', '--purge', 'microstack']), 1)
 | 
			
		||||
 | 
			
		||||
        # Retry with sudo (should succeed).
 | 
			
		||||
        check(*prefix, 'sudo', 'snap', 'remove', '--purge', 'microstack')
 | 
			
		||||
        self._localhost.check_call(
 | 
			
		||||
            ['sudo', 'snap', 'remove', '--purge', 'microstack'])
 | 
			
		||||
 | 
			
		||||
        # Verify that MicroStack is gone.
 | 
			
		||||
        self.assertFalse(call(*prefix, 'snap', 'list', 'microstack'))
 | 
			
		||||
        self.assertEqual(self._localhost.call(
 | 
			
		||||
            ['snap', 'list', 'microstack']), 1)
 | 
			
		||||
 | 
			
		||||
        # Verify that bridge is gone.
 | 
			
		||||
        self.assertFalse('br-ex' in check_output(*prefix, 'ip', 'a'))
 | 
			
		||||
        self.assertFalse(
 | 
			
		||||
            'br-ex' in self._localhost.check_output(
 | 
			
		||||
                ['ip', 'a']).decode('utf-8'))
 | 
			
		||||
 | 
			
		||||
        # We made it to the end. Set passed to True!
 | 
			
		||||
        self.passed = True
 | 
			
		||||
 
 | 
			
		||||
@@ -14,104 +14,126 @@ import json
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
import unittest
 | 
			
		||||
import netifaces
 | 
			
		||||
import tenacity
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
sys.path.append(os.getcwd())
 | 
			
		||||
 | 
			
		||||
from tests.framework import Framework, check, check_output, call  # noqa E402
 | 
			
		||||
from tests.framework import Framework  # noqa E402
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
os.environ['MULTIPASS'] = 'true'  # TODO better way to do this.
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
logger.setLevel(logging.DEBUG)
 | 
			
		||||
stream = logging.StreamHandler()
 | 
			
		||||
formatter = logging.Formatter(
 | 
			
		||||
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 | 
			
		||||
stream.setFormatter(formatter)
 | 
			
		||||
logger.addHandler(stream)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestCluster(Framework):
 | 
			
		||||
 | 
			
		||||
    def test_cluster(self):
 | 
			
		||||
        openstack_cmd = '/snap/bin/microstack.openstack'
 | 
			
		||||
        control_host = self._localhost
 | 
			
		||||
        control_host.install_microstack(path='microstack_ussuri_amd64.snap')
 | 
			
		||||
 | 
			
		||||
        # After the setUp step, we should have a control node running
 | 
			
		||||
        # in a multipass vm. Let's look up its cluster password and ip
 | 
			
		||||
        # address.
 | 
			
		||||
        # Get an IP address on the lxdbr0 bridge and use it for the
 | 
			
		||||
        # control IP so that the tunnel ports of the compute node target the
 | 
			
		||||
        # IP on the lxdbr0 subnet. Using netifaces only work for the
 | 
			
		||||
        # localhost control node scenario.
 | 
			
		||||
        ifaddrs = netifaces.ifaddresses('lxdbr0')[netifaces.AF_INET]
 | 
			
		||||
        # We expect only one address on this interface, if multiple are present
 | 
			
		||||
        # there is something wrong and we should fail the test and reassess.
 | 
			
		||||
        self.assertEqual(len(ifaddrs), 1)
 | 
			
		||||
        control_ip = ifaddrs[0]['addr']
 | 
			
		||||
 | 
			
		||||
        openstack = '/snap/bin/microstack.openstack'
 | 
			
		||||
        control_host = self.get_host()
 | 
			
		||||
        control_host.install()
 | 
			
		||||
        control_host.init(['--control'])
 | 
			
		||||
        control_host.init_microstack(['--auto', '--control',
 | 
			
		||||
                                      f'--default-source-ip={control_ip}'])
 | 
			
		||||
 | 
			
		||||
        control_prefix = control_host.prefix
 | 
			
		||||
        cluster_password = check_output(*control_prefix, 'sudo', 'snap',
 | 
			
		||||
                                        'get', 'microstack',
 | 
			
		||||
                                        'config.cluster.password')
 | 
			
		||||
        control_ip = check_output(*control_prefix, 'sudo', 'snap',
 | 
			
		||||
                                  'get', 'microstack',
 | 
			
		||||
                                  'config.network.control-ip')
 | 
			
		||||
        compute_host = self.add_lxd_host('focal')
 | 
			
		||||
        compute_host.copy_to('microstack_ussuri_amd64.snap', '/root/')
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(cluster_password)
 | 
			
		||||
        self.assertTrue(control_ip)
 | 
			
		||||
        # snapd does not come up immediately in the container.
 | 
			
		||||
        @tenacity.retry(wait=tenacity.wait_fixed(1),
 | 
			
		||||
                        stop=tenacity.stop_after_attempt(10))
 | 
			
		||||
        def wait_snapd():
 | 
			
		||||
            compute_host.check_call(['sudo', 'snap', 'list'])
 | 
			
		||||
 | 
			
		||||
        compute_host = self.add_host()
 | 
			
		||||
        compute_host.install()
 | 
			
		||||
        wait_snapd()
 | 
			
		||||
 | 
			
		||||
        compute_machine = compute_host.machine
 | 
			
		||||
        compute_prefix = compute_host.prefix
 | 
			
		||||
        # wait for an IPv4 address to appear on the container interface
 | 
			
		||||
        @tenacity.retry(wait=tenacity.wait_fixed(1),
 | 
			
		||||
                        stop=tenacity.stop_after_attempt(10))
 | 
			
		||||
        def wait_addr():
 | 
			
		||||
            logger.debug('Checking for an eth0 interface addresses presence'
 | 
			
		||||
                         ' in the container.')
 | 
			
		||||
            cmd = ['ip', '-4', '-o', 'addr', 'show', 'eth0']
 | 
			
		||||
            ip_out = compute_host.check_output(cmd).decode('utf-8')
 | 
			
		||||
            logger.debug(f'{" ".join(cmd)} output:\n{ip_out}')
 | 
			
		||||
 | 
			
		||||
        wait_addr()
 | 
			
		||||
 | 
			
		||||
        compute_host.install_microstack(path='microstack_ussuri_amd64.snap')
 | 
			
		||||
 | 
			
		||||
        # TODO add the following to args for init
 | 
			
		||||
        check(*compute_prefix, 'sudo', 'snap', 'set', 'microstack',
 | 
			
		||||
              'config.network.control-ip={}'.format(control_ip))
 | 
			
		||||
        compute_host.check_call([
 | 
			
		||||
            'sudo', 'snap', 'set', 'microstack',
 | 
			
		||||
            f'config.network.control-ip={control_ip}'])
 | 
			
		||||
 | 
			
		||||
        check(*compute_prefix, 'sudo', 'microstack.init', '--compute',
 | 
			
		||||
              '--cluster-password', cluster_password, '--debug')
 | 
			
		||||
        connection_string = control_host.check_output([
 | 
			
		||||
            'sudo', 'microstack', 'add-compute'
 | 
			
		||||
        ]).decode('utf-8')
 | 
			
		||||
        self.assertTrue(connection_string)
 | 
			
		||||
 | 
			
		||||
        compute_host.check_call([
 | 
			
		||||
            'sudo', 'microstack.init', '--auto',
 | 
			
		||||
            '--compute', '--join', connection_string, '--debug'
 | 
			
		||||
        ])
 | 
			
		||||
 | 
			
		||||
        # Verify that our services look setup properly on compute node.
 | 
			
		||||
        services = check_output(
 | 
			
		||||
            *compute_prefix, 'systemctl', 'status', 'snap.microstack.*',
 | 
			
		||||
            '--no-page')
 | 
			
		||||
        services = compute_host.check_output([
 | 
			
		||||
            'systemctl', 'status', 'snap.microstack.*',
 | 
			
		||||
            '--no-page']).decode('utf-8')
 | 
			
		||||
 | 
			
		||||
        self.assertTrue('nova-compute' in services)
 | 
			
		||||
        self.assertFalse('keystone-' in services)
 | 
			
		||||
 | 
			
		||||
        check(*compute_prefix, '/snap/bin/microstack.launch', 'cirros',
 | 
			
		||||
              '--name', 'breakfast', '--retry',
 | 
			
		||||
              '--availability-zone', 'nova:{}'.format(compute_machine))
 | 
			
		||||
        compute_fqdn = compute_host.check_output([
 | 
			
		||||
            'hostname', '-f']).decode('utf-8')
 | 
			
		||||
 | 
			
		||||
        # TODO: verify horizon dashboard on control node.
 | 
			
		||||
        instance_name = 'test-instance'
 | 
			
		||||
        # Launch from the control host but schedule to the compute host.
 | 
			
		||||
        control_host.check_call([
 | 
			
		||||
            '/snap/bin/microstack.launch', 'cirros',
 | 
			
		||||
            '--name', instance_name, '--retry',
 | 
			
		||||
            '--availability-zone', f'nova:{compute_fqdn}'])
 | 
			
		||||
 | 
			
		||||
        # Verify endpoints
 | 
			
		||||
        compute_ip = check_output(*compute_prefix, 'sudo', 'snap',
 | 
			
		||||
                                  'get', 'microstack',
 | 
			
		||||
                                  'config.network.compute-ip')
 | 
			
		||||
        compute_ip = compute_host.check_output([
 | 
			
		||||
            'sudo', 'snap',
 | 
			
		||||
            'get', 'microstack',
 | 
			
		||||
            'config.network.compute-ip'
 | 
			
		||||
        ]).decode('utf-8')
 | 
			
		||||
        self.assertFalse(compute_ip == control_ip)
 | 
			
		||||
 | 
			
		||||
        # Ping the instance
 | 
			
		||||
        ip = None
 | 
			
		||||
        servers = check_output(*compute_prefix, openstack,
 | 
			
		||||
                               'server', 'list', '--format', 'json')
 | 
			
		||||
        servers = compute_host.check_output([
 | 
			
		||||
            openstack_cmd,
 | 
			
		||||
            'server', 'list', '--format', 'json'
 | 
			
		||||
        ]).decode('utf-8')
 | 
			
		||||
        servers = json.loads(servers)
 | 
			
		||||
        for server in servers:
 | 
			
		||||
            if server['Name'] == 'breakfast':
 | 
			
		||||
            if server['Name'] == instance_name:
 | 
			
		||||
                ip = server['Networks'].split(",")[1].strip()
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(ip)
 | 
			
		||||
 | 
			
		||||
        pings = 1
 | 
			
		||||
        max_pings = 60  # ~1 minutes
 | 
			
		||||
        # Ping the machine from the control node (we don't have
 | 
			
		||||
        # networking wired up for the other nodes).
 | 
			
		||||
        while not call(*control_prefix, 'ping', '-c1', '-w1', ip):
 | 
			
		||||
            pings += 1
 | 
			
		||||
            if pings > max_pings:
 | 
			
		||||
                self.assertFalse(
 | 
			
		||||
                    True,
 | 
			
		||||
                    msg='Max pings reached for instance on {}!'.format(
 | 
			
		||||
                        compute_machine))
 | 
			
		||||
 | 
			
		||||
        control_host.check_call(['ping', '-c10', '-w11', ip])
 | 
			
		||||
        self.passed = True
 | 
			
		||||
 | 
			
		||||
        # Compute machine cleanup
 | 
			
		||||
        check('sudo', 'multipass', 'delete', compute_machine)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    # Run our tests, ignoring deprecation warnings and warnings about
 | 
			
		||||
    # unclosed sockets. (TODO: setup a selenium server so that we can
 | 
			
		||||
    # move from PhantomJS, which is deprecated, to to Selenium headless.)
 | 
			
		||||
    unittest.main(warnings='ignore')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
import uuid
 | 
			
		||||
import secrets
 | 
			
		||||
import argparse
 | 
			
		||||
@@ -106,7 +107,8 @@ def add_compute():
 | 
			
		||||
    # Print the connection string and an expiration notice to the user.
 | 
			
		||||
    print('Use the following connection string to add a new compute node'
 | 
			
		||||
          f' to the cluster (valid for {VALIDITY_PERIOD.minutes} minutes from'
 | 
			
		||||
          f' this moment):\n{connection_string}')
 | 
			
		||||
          f' this moment):', file=sys.stderr)
 | 
			
		||||
    print(connection_string)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
import urllib3
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
@@ -65,7 +66,8 @@ def join():
 | 
			
		||||
        print('An authorization failure has occurred while joining the'
 | 
			
		||||
              ' the cluster: please make sure the connection string'
 | 
			
		||||
              ' was entered as returned by the "add-compute" command'
 | 
			
		||||
              ' and that it was used before its expiration time.')
 | 
			
		||||
              ' and that it was used before its expiration time.',
 | 
			
		||||
              file=sys.stderr)
 | 
			
		||||
        if response_data:
 | 
			
		||||
            message = json.loads(response_data)['message']
 | 
			
		||||
            raise UnauthorizedRequestError(message)
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ import argparse
 | 
			
		||||
import logging
 | 
			
		||||
import sys
 | 
			
		||||
import socket
 | 
			
		||||
import ipaddress
 | 
			
		||||
 | 
			
		||||
from functools import wraps
 | 
			
		||||
 | 
			
		||||
@@ -42,6 +43,7 @@ from init.shell import (
 | 
			
		||||
    check,
 | 
			
		||||
    check_output,
 | 
			
		||||
    config_set,
 | 
			
		||||
    fallback_source_address,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from init import questions
 | 
			
		||||
@@ -68,6 +70,16 @@ def check_file_size_positive(value):
 | 
			
		||||
    return ival
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_source_ip_address_valid(value):
 | 
			
		||||
    try:
 | 
			
		||||
        addr = ipaddress.ip_address(value)
 | 
			
		||||
    except ValueError as e:
 | 
			
		||||
        raise argparse.ArgumentTypeError(
 | 
			
		||||
            'Invalid source IP address provided in as an argument.'
 | 
			
		||||
        ) from e
 | 
			
		||||
    return addr
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_init_args():
 | 
			
		||||
    parser = argparse.ArgumentParser()
 | 
			
		||||
    parser.add_argument('--auto', '-a', action='store_true',
 | 
			
		||||
@@ -94,6 +106,13 @@ def parse_init_args():
 | 
			
		||||
            help=('File size in GB (10^9) of a file to be exposed as a loop'
 | 
			
		||||
                  ' device for the Cinder LVM backend.')
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument('--default-source-ip',
 | 
			
		||||
                        dest='default_source_ip',
 | 
			
		||||
                        type=check_source_ip_address_valid,
 | 
			
		||||
                        default=fallback_source_address(),
 | 
			
		||||
                        help='The IP address to be used by MicroStack'
 | 
			
		||||
                             ' services as a source IP where possible. This'
 | 
			
		||||
                             ' option can be useful for multi-homed nodes.')
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
    return args
 | 
			
		||||
 | 
			
		||||
@@ -124,6 +143,9 @@ def process_init_args(args):
 | 
			
		||||
        config_set(**{
 | 
			
		||||
            'config.cluster.connection-string.raw': args.connection_string})
 | 
			
		||||
 | 
			
		||||
    config_set(**{
 | 
			
		||||
        'config.network.default-source-ip': args.default_source_ip})
 | 
			
		||||
 | 
			
		||||
    if args.auto and not args.control and not args.connection_string:
 | 
			
		||||
        raise ValueError('The connection string parameter must be specified'
 | 
			
		||||
                         ' for compute nodes.')
 | 
			
		||||
@@ -157,7 +179,6 @@ def init() -> None:
 | 
			
		||||
        questions.DnsDomain(),
 | 
			
		||||
        questions.NetworkSettings(),
 | 
			
		||||
        questions.OsPassword(),  # TODO: turn this off if COMPUTE.
 | 
			
		||||
        questions.ForceQemu(),
 | 
			
		||||
        # The following are not yet implemented:
 | 
			
		||||
        # questions.VmSwappiness(),
 | 
			
		||||
        # questions.FileHandleLimits(),
 | 
			
		||||
 
 | 
			
		||||
@@ -257,38 +257,6 @@ class OsPassword(ConfigQuestion):
 | 
			
		||||
        _env['keystone_password'] = answer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ForceQemu(Question):
 | 
			
		||||
    _type = 'boolean'
 | 
			
		||||
    config_key = 'config.host.check-qemu'
 | 
			
		||||
 | 
			
		||||
    def yes(self, answer: str) -> None:
 | 
			
		||||
        """Possibly force us to use qemu emulation rather than kvm."""
 | 
			
		||||
 | 
			
		||||
        cpuinfo = check_output('cat', '/proc/cpuinfo')
 | 
			
		||||
        if 'vmx' in cpuinfo or 'svm' in cpuinfo:
 | 
			
		||||
            # We have processor extensions installed. No need to Force
 | 
			
		||||
            # Qemu emulation.
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        _path = '{SNAP_COMMON}/etc/nova/nova.conf.d/hypervisor.conf'.format(
 | 
			
		||||
            **_env)
 | 
			
		||||
 | 
			
		||||
        with open(_path, 'w') as _file:
 | 
			
		||||
            _file.write("""\
 | 
			
		||||
[DEFAULT]
 | 
			
		||||
compute_driver = libvirt.LibvirtDriver
 | 
			
		||||
 | 
			
		||||
[workarounds]
 | 
			
		||||
disable_rootwrap = True
 | 
			
		||||
 | 
			
		||||
[libvirt]
 | 
			
		||||
virt_type = qemu
 | 
			
		||||
cpu_mode = host-model
 | 
			
		||||
""")
 | 
			
		||||
 | 
			
		||||
        # TODO: restart nova services when re-running this after init.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VmSwappiness(Question):
 | 
			
		||||
 | 
			
		||||
    _type = 'boolean'
 | 
			
		||||
@@ -454,6 +422,7 @@ class NovaHypervisor(Question):
 | 
			
		||||
 | 
			
		||||
    def yes(self, answer):
 | 
			
		||||
        log.info('Configuring nova compute hypervisor ...')
 | 
			
		||||
        self._maybe_enable_emulation()
 | 
			
		||||
        enable('libvirtd')
 | 
			
		||||
        enable('virtlogd')
 | 
			
		||||
        enable('nova-compute')
 | 
			
		||||
@@ -464,6 +433,66 @@ class NovaHypervisor(Question):
 | 
			
		||||
        disable('virtlogd')
 | 
			
		||||
        disable('nova-compute')
 | 
			
		||||
 | 
			
		||||
    def _maybe_enable_emulation(self):
 | 
			
		||||
        log.info('Checking virtualization extensions presence on the host')
 | 
			
		||||
        # Use KVM if it is supported, alternatively fall back to software
 | 
			
		||||
        # emulation.
 | 
			
		||||
        if self._is_hw_virt_supported():
 | 
			
		||||
            log.info('Hardware virtualization is supported - KVM will be used'
 | 
			
		||||
                     ' for Nova instances')
 | 
			
		||||
            shell.config_set(**{'config.nova.virt-type': 'kvm'})
 | 
			
		||||
            shell.config_set(**{'config.nova.cpu-mode': 'host-passthrough'})
 | 
			
		||||
        else:
 | 
			
		||||
            log.warning('Hardware virtualization is not supported - software'
 | 
			
		||||
                        ' emulation will be used for Nova instances')
 | 
			
		||||
            shell.config_set(**{'config.nova.virt-type': 'qemu'})
 | 
			
		||||
            shell.config_set(**{'config.nova.cpu-mode': 'host-passthrough'})
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _is_hw_virt_supported():
 | 
			
		||||
        # Sample lscpu outputs: util-linux/tests/expected/lscpu/
 | 
			
		||||
        cpu_info = json.loads(check_output('lscpu', '-J'))['lscpu']
 | 
			
		||||
        architecture = next(filter(lambda x: x['field'] == 'Architecture:',
 | 
			
		||||
                                   cpu_info), None)['data'].split()
 | 
			
		||||
        flags = next(filter(lambda x: x['field'] == 'Flags:', cpu_info),
 | 
			
		||||
                     None)
 | 
			
		||||
        if flags is not None:
 | 
			
		||||
            flags = flags['data'].split()
 | 
			
		||||
 | 
			
		||||
        vendor_id = next(filter(lambda x: x['field'] == 'Vendor ID:',
 | 
			
		||||
                                cpu_info), None)
 | 
			
		||||
        if vendor_id is not None:
 | 
			
		||||
            vendor_id = vendor_id['data']
 | 
			
		||||
 | 
			
		||||
        # Mimic virt-host-validate code (from libvirt) and assume nested
 | 
			
		||||
        # support on ppc64 LE or BE.
 | 
			
		||||
        if architecture in ['ppc64', 'ppc64le']:
 | 
			
		||||
            return True
 | 
			
		||||
        elif vendor_id is not None and flags is not None:
 | 
			
		||||
            if vendor_id == 'AuthenticAMD' and 'svm' in flags:
 | 
			
		||||
                return True
 | 
			
		||||
            elif vendor_id == 'GenuineIntel' and 'vmx' in flags:
 | 
			
		||||
                return True
 | 
			
		||||
            elif vendor_id == 'IBM/S390' and 'sie' in flags:
 | 
			
		||||
                return True
 | 
			
		||||
            elif vendor_id == 'ARM':
 | 
			
		||||
                # ARM 8.3-A added nested virtualization support but it is yet
 | 
			
		||||
                # to land upstream https://lwn.net/Articles/812280/ at the time
 | 
			
		||||
                # of writing (Nov 2020).
 | 
			
		||||
                log.warning('Nested virtualization is not supported on ARM'
 | 
			
		||||
                            ' - will use emulation')
 | 
			
		||||
                return False
 | 
			
		||||
            else:
 | 
			
		||||
                log.warning('Unable to determine hardware virtualization'
 | 
			
		||||
                            f' support by CPU vendor id "{vendor_id}":'
 | 
			
		||||
                            ' assuming it is not supported.')
 | 
			
		||||
                return False
 | 
			
		||||
        else:
 | 
			
		||||
            log.warning('Unable to determine hardware virtualization support'
 | 
			
		||||
                        ' by the output of lscpu: assuming it is not'
 | 
			
		||||
                        ' supported')
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NovaSpiceConsoleSetup(Question):
 | 
			
		||||
    """Run the Spice HTML5 console proxy service"""
 | 
			
		||||
 
 | 
			
		||||
@@ -2,13 +2,14 @@ import logging
 | 
			
		||||
import msgpack
 | 
			
		||||
import re
 | 
			
		||||
import netaddr
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
from cryptography.hazmat.primitives import hashes
 | 
			
		||||
from typing import Tuple
 | 
			
		||||
 | 
			
		||||
from init.questions.question import Question, InvalidAnswer
 | 
			
		||||
from init.shell import (
 | 
			
		||||
    fetch_ip_address,
 | 
			
		||||
    default_source_address,
 | 
			
		||||
    config_get,
 | 
			
		||||
    config_set,
 | 
			
		||||
)
 | 
			
		||||
@@ -39,7 +40,8 @@ class Role(Question):
 | 
			
		||||
            if role in self._valid_roles:
 | 
			
		||||
                return role
 | 
			
		||||
 | 
			
		||||
            print('The role must be either "control" or "compute".')
 | 
			
		||||
            print('The role must be either "control" or "compute".',
 | 
			
		||||
                  file=sys.stderr)
 | 
			
		||||
 | 
			
		||||
        raise InvalidAnswer('Too many failed attempts.')
 | 
			
		||||
 | 
			
		||||
@@ -58,7 +60,8 @@ class ConnectionString(Question):
 | 
			
		||||
        except TypeError:
 | 
			
		||||
            print('The connection string contains non-ASCII'
 | 
			
		||||
                  ' characters please make sure you entered'
 | 
			
		||||
                  ' it as returned by the add-compute command.')
 | 
			
		||||
                  ' it as returned by the add-compute command.',
 | 
			
		||||
                  file=sys.stderr)
 | 
			
		||||
            return answer, False
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
@@ -66,24 +69,28 @@ class ConnectionString(Question):
 | 
			
		||||
        except msgpack.exceptions.ExtraData:
 | 
			
		||||
            print('The connection string contains extra data'
 | 
			
		||||
                  ' characters please make sure you entered'
 | 
			
		||||
                  ' it as returned by the add-compute command.')
 | 
			
		||||
                  ' it as returned by the add-compute command.',
 | 
			
		||||
                  file=sys.stderr)
 | 
			
		||||
            return answer, False
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            print('The connection string contains extra data'
 | 
			
		||||
                  ' characters please make sure you entered'
 | 
			
		||||
                  ' it as returned by the add-compute command.')
 | 
			
		||||
                  ' it as returned by the add-compute command.',
 | 
			
		||||
                  file=sys.stderr)
 | 
			
		||||
            return answer, False
 | 
			
		||||
        except msgpack.exceptions.FormatError:
 | 
			
		||||
            print('The connection string format is invalid'
 | 
			
		||||
                  ' please make sure you entered'
 | 
			
		||||
                  ' it as returned by the add-compute command.')
 | 
			
		||||
                  ' it as returned by the add-compute command.',
 | 
			
		||||
                  file=sys.stderr)
 | 
			
		||||
            return answer, False
 | 
			
		||||
        except Exception:
 | 
			
		||||
            print('An unexpeted error has occured while trying'
 | 
			
		||||
                  ' to decode the connection string. Please'
 | 
			
		||||
                  ' make sure you entered it as returned by'
 | 
			
		||||
                  ' the add-compute command and raise an'
 | 
			
		||||
                  ' issue if the error persists')
 | 
			
		||||
                  ' issue if the error persists',
 | 
			
		||||
                  file=sys.stderr)
 | 
			
		||||
            return answer, False
 | 
			
		||||
 | 
			
		||||
        # Perform token field validation as well so that the rest of
 | 
			
		||||
@@ -103,7 +110,7 @@ class ConnectionString(Question):
 | 
			
		||||
                self._validate_hostname(hostname)
 | 
			
		||||
            except ValueError as e:
 | 
			
		||||
                print(f'The hostname {hostname} provided in the connection'
 | 
			
		||||
                      f' string is invalid: {str(e)}')
 | 
			
		||||
                      f' string is invalid: {str(e)}', file=sys.stderr)
 | 
			
		||||
                return answer, False
 | 
			
		||||
 | 
			
		||||
        fingerprint = conn_info.get('fingerprint')
 | 
			
		||||
@@ -111,7 +118,8 @@ class ConnectionString(Question):
 | 
			
		||||
            self._validate_fingerprint(fingerprint)
 | 
			
		||||
        except ValueError as e:
 | 
			
		||||
            print('The clustering service TLS certificate fingerprint provided'
 | 
			
		||||
                  f' in the connection string is invalid: {str(e)}')
 | 
			
		||||
                  f' in the connection string is invalid: {str(e)}',
 | 
			
		||||
                  file=sys.stderr)
 | 
			
		||||
            return answer, False
 | 
			
		||||
 | 
			
		||||
        credential_id = conn_info.get('id')
 | 
			
		||||
@@ -119,7 +127,7 @@ class ConnectionString(Question):
 | 
			
		||||
            self._validate_credential_id(credential_id)
 | 
			
		||||
        except ValueError as e:
 | 
			
		||||
            print('The credential id provided in the connection string is'
 | 
			
		||||
                  f' invalid: {str(e)}')
 | 
			
		||||
                  f' invalid: {str(e)}', file=sys.stderr)
 | 
			
		||||
            return answer, False
 | 
			
		||||
 | 
			
		||||
        credential_secret = conn_info.get('secret')
 | 
			
		||||
@@ -127,7 +135,7 @@ class ConnectionString(Question):
 | 
			
		||||
            self._validate_credential_secret(credential_secret)
 | 
			
		||||
        except ValueError as e:
 | 
			
		||||
            print('The credential secret provided in the connection string is'
 | 
			
		||||
                  f' invalid: {str(e)}')
 | 
			
		||||
                  f' invalid: {str(e)}', file=sys.stderr)
 | 
			
		||||
            return answer, False
 | 
			
		||||
 | 
			
		||||
        self._conn_info = conn_info
 | 
			
		||||
@@ -224,7 +232,7 @@ class ControlIp(Question):
 | 
			
		||||
 | 
			
		||||
    def _load(self):
 | 
			
		||||
        if config_get(Role.config_key) == 'control':
 | 
			
		||||
            return fetch_ip_address() or super()._load()
 | 
			
		||||
            return default_source_address() or super()._load()
 | 
			
		||||
        return super()._load()
 | 
			
		||||
 | 
			
		||||
    def ask(self):
 | 
			
		||||
@@ -245,7 +253,7 @@ class ComputeIp(Question):
 | 
			
		||||
    def _load(self):
 | 
			
		||||
        role = config_get(Role.config_key)
 | 
			
		||||
        if role == 'compute':
 | 
			
		||||
            return fetch_ip_address() or super().load()
 | 
			
		||||
            return default_source_address() or super().load()
 | 
			
		||||
 | 
			
		||||
        return super()._load()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -199,7 +199,13 @@ def download(url: str, output: str) -> None:
 | 
			
		||||
    wget.download(url, output)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def fetch_ip_address():
 | 
			
		||||
def fallback_source_address():
 | 
			
		||||
    '''Get an ip address through which the default gateway is accessible.
 | 
			
		||||
 | 
			
		||||
    Note that Linux kernel allows multiple default gateways to be present with
 | 
			
		||||
    the same or different metrics which may lead to unexpected behaviors. This
 | 
			
		||||
    situation is unlikely but needs to be taken into account.
 | 
			
		||||
    '''
 | 
			
		||||
    try:
 | 
			
		||||
        interface = netifaces.gateways()['default'][netifaces.AF_INET][1]
 | 
			
		||||
        return netifaces.ifaddresses(interface)[netifaces.AF_INET][0]['addr']
 | 
			
		||||
@@ -208,6 +214,11 @@ def fetch_ip_address():
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def default_source_address():
 | 
			
		||||
    '''Get a default source address.'''
 | 
			
		||||
    return config_get('config.network.default-source-ip')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def default_network():
 | 
			
		||||
    """Get info about the default netowrk.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								tox.ini
									
									
									
									
									
								
							@@ -28,6 +28,7 @@ commands =
 | 
			
		||||
    # Specify tests in sequence, as they can't run in parallel if not
 | 
			
		||||
    # using multipass.
 | 
			
		||||
    bash -c "unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ; {toxinidir}/tests/test_basic.py"
 | 
			
		||||
    bash -c "unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ; {toxinidir}/tests/test_cluster.py"
 | 
			
		||||
 | 
			
		||||
[testenv:multipass]
 | 
			
		||||
# Default testing environment for a human operated machine. Builds the
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user