##############################################################################
#
# Copyright (c) 2018 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# 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, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

import http.client as httplib
import http.server
import json
import os
import glob
import hashlib
import psutil
import re
import requests
import slapos.util
import sqlite3
from urllib.parse import parse_qs, urlparse
import unittest
import subprocess
import tempfile
import socketserver as SocketServer
import multiprocessing
import time
import shutil
import sys

from slapos.qemuqmpclient import QemuQMPWrapper
from slapos.proxy.db_version import DB_VERSION
from slapos.recipe.librecipe import generateHashFromFiles
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
from slapos.slap.standalone import SlapOSNodeCommandError
from slapos.testing.utils import findFreeTCPPort

has_kvm = os.access('/dev/kvm', os.R_OK | os.W_OK)
skipUnlessKvm = unittest.skipUnless(has_kvm, 'kvm not loaded or not allowed')

if has_kvm:
  setUpModule, InstanceTestCase = makeModuleSetUpAndTestCaseClass(
    os.path.abspath(
      os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
  # XXX Keep using slapos node instance --all, because of missing promises
  InstanceTestCase.slap._force_slapos_node_instance_all = True
else:
  setUpModule, InstanceTestCase = None, unittest.TestCase

  class SanityCheckTestCase(unittest.TestCase):
    def test_kvm_sanity_check(self):
      self.fail('This environment is not usable for kvm testing,'
                ' as it lacks kvm_intel kernel module')

bootstrap_common_param_dict = {
    # the bootstrap script is vm-bootstrap
    "bootstrap-script-url":
    "http://shacache.org/shacache/05105cd25d1ad798b71fd46a206c9b73da2c285a078"
    "af33d0e739525a595886785725a68811578bc21f75d0a97700a66d5e75bce5b2721ca455"
    "6a0734cb13e65#c98825aa1b6c8087914d2bfcafec3058",
    "slave-frontend": {
        "slave-frontend-dict": {}
    },
    "authorized-keys": [
        "ssh-rsa %s key_one" % ("A" * 372),
        "ssh-rsa %s key_two" % ("B" * 372),
        "ssh-rsa %s key_three" % ("C" * 372)
    ],
    "fw-restricted-access": "off",
    "fw-authorized-sources": [],
    "fw-reject-sources": ["10.32.0.0/13"]
}

bootstrap_machine_param_dict = {
    "computer-guid": "local",
    "disable-ansible-promise": True,
    "state": "started",
    "auto-ballooning": True,
    "ram-size": 4096,
    "cpu-count": 2,
    "disk-size": 50,
    "virtual-hard-drive-url":
    "http://shacache.org/shacache/a869d906fcd0af5091d5104451a2b86736485ae38e5"
    "c4388657bb957c25593b98378ed125f593683e7fda7e0dd485a376a0ce29dcbaa8d60766"
    "e1f67a7ef7b96",
    "virtual-hard-drive-md5sum": "9ffd690a5fcb4fa56702f2b99183e493",
    "virtual-hard-drive-gzipped": True,
    "use-tap": True,
    "use-nat": True,
    "nat-restrict-mode": True,
    "enable-vhost": True,
    "external-disk-number": 1,
    "external-disk-size": 100,
    "external-disk-format": "qcow2",
    "enable-monitor": True,
    "keyboard-layout-language": "fr"
}


class KvmMixin:
  def getConnectionParameterDictJson(self):
    return json.loads(
      self.computer_partition.getConnectionParameterDict()['_'])

  def getProcessInfo(self):
    hash_value = generateHashFromFiles([
      os.path.join(self.computer_partition_root_path, hash_file)
      for hash_file in [
        'software_release/buildout.cfg',
      ]
    ])
    # find bin/kvm_raw
    kvm_raw_list = glob.glob(
      os.path.join(self.slap.instance_directory, '*', 'bin', 'kvm_raw'))
    self.assertEqual(1, len(kvm_raw_list))  # allow to work only with one
    hash_file_list = [
      kvm_raw_list[0],
      'software_release/buildout.cfg',
    ]
    kvm_hash_value = generateHashFromFiles([
      os.path.join(self.computer_partition_root_path, hash_file)
      for hash_file in hash_file_list
    ])
    with self.slap.instance_supervisor_rpc as supervisor:
      running_process_info = '\n'.join(sorted(
        '%(group)s:%(name)s %(statename)s' % q for q
        in supervisor.getAllProcessInfo()
        if q['name'] != 'watchdog' and q['group'] != 'watchdog'))
    return running_process_info.replace(
      hash_value, '{hash}').replace(kvm_hash_value, '{kvm-hash-value}')

  def raising_waitForInstance(self, max_retry):
    with self.assertRaises(SlapOSNodeCommandError):
      self.slap.waitForInstance(max_retry=max_retry)

  def rerequestInstance(self, parameter_dict, state='started'):
    software_url = self.getSoftwareURL()
    software_type = self.getInstanceSoftwareType()
    return self.slap.request(
        software_release=software_url,
        software_type=software_type,
        partition_reference=self.default_partition_reference,
        partition_parameter_kw=parameter_dict,
        state=state)


class KvmMixinJson:
  @classmethod
  def getInstanceParameterDict(cls):
    return {
      '_': json.dumps(super().getInstanceParameterDict())}

  def rerequestInstance(self, parameter_dict, *args, **kwargs):
    return super().rerequestInstance(
      parameter_dict={'_': json.dumps(parameter_dict)},
      *args, **kwargs
    )


@skipUnlessKvm
class TestInstance(InstanceTestCase, KvmMixin):
  __partition_reference__ = 'i'

  def test(self):
    connection_parameter_dict = self.getConnectionParameterDictJson()
    present_key_list = []
    assert_key_list = [
     'backend-url', 'url', 'monitor-setup-url', 'ipv6-network-info',
     'tap-ipv4', 'tap-ipv6']
    for k in assert_key_list:
      if k in connection_parameter_dict:
        present_key_list.append(k)
        connection_parameter_dict.pop(k)
    self.assertEqual(
      connection_parameter_dict,
      {
        'ipv6': self._ipv6_address,
        'maximum-extra-disk-amount': '0',
        'monitor-base-url': f'https://[{self._ipv6_address}]:8026',
        'nat-rule-port-tcp-22': f'{self._ipv6_address} : 10022',
        'nat-rule-port-tcp-443': f'{self._ipv6_address} : 10443',
        'nat-rule-port-tcp-80': f'{self._ipv6_address} : 10080',
      }
    )
    self.assertEqual(set(present_key_list), set(assert_key_list))
    self.assertEqual(
      """i0:6tunnel-10022-{hash}-on-watch RUNNING
i0:6tunnel-10080-{hash}-on-watch RUNNING
i0:6tunnel-10443-{hash}-on-watch RUNNING
i0:bootstrap-monitor EXITED
i0:certificate_authority-{hash}-on-watch RUNNING
i0:crond-{hash}-on-watch RUNNING
i0:kvm-{kvm-hash-value}-on-watch RUNNING
i0:kvm_controller EXITED
i0:monitor-httpd-{hash}-on-watch RUNNING
i0:monitor-httpd-graceful EXITED
i0:nginx-graceful EXITED
i0:nginx-on-watch RUNNING
i0:whitelist-domains-download-{hash} RUNNING
i0:whitelist-firewall-{hash} RUNNING""",
      self.getProcessInfo()
    )


@skipUnlessKvm
class TestInstanceJson(
  KvmMixinJson, TestInstance):
  pass


@skipUnlessKvm
class TestMemoryManagement(InstanceTestCase, KvmMixin):
  __partition_reference__ = 'i'

  def getKvmProcessInfo(self, switch_list):
    return_list = []
    with self.slap.instance_supervisor_rpc as instance_supervisor:
      kvm_pid = [q for q in instance_supervisor.getAllProcessInfo()
                 if 'kvm-' in q['name']][0]['pid']
      kvm_process = psutil.Process(kvm_pid)
      get_next = False
      for entry in kvm_process.cmdline():
        if get_next:
          return_list.append(entry)
          get_next = False
        elif entry in switch_list:
          get_next = True
    return kvm_pid, return_list

  def test(self):
    kvm_pid_1, info_list = self.getKvmProcessInfo(['-smp', '-m'])
    self.assertEqual(
      ['2,maxcpus=3', '4096M,slots=128,maxmem=4608M'],
      info_list
    )
    self.rerequestInstance({
      'ram-size': '1536',
      'cpu-count': '2',
    })
    self.slap.waitForInstance(max_retry=10)
    kvm_pid_2, info_list = self.getKvmProcessInfo(['-smp', '-m'])
    self.assertEqual(
      ['2,maxcpus=3', '1536M,slots=128,maxmem=2048M'],
      info_list
    )

    # assert that process was restarted
    self.assertNotEqual(kvm_pid_1, kvm_pid_2, "Unexpected: KVM not restarted")

  def tearDown(self):
    self.rerequestInstance({})
    self.slap.waitForInstance(max_retry=10)

  def test_enable_device_hotplug(self):
    def getHotpluggedCpuRamValue():
      qemu_wrapper = QemuQMPWrapper(os.path.join(
        self.computer_partition_root_path, 'var', 'qmp_socket'))
      ram_mb = sum(
        q['size']
         for q in qemu_wrapper.getMemoryInfo()['hotplugged']) / 1024 / 1024
      cpu_count = len(
        [q['CPU'] for q in qemu_wrapper.getCPUInfo()['hotplugged']])
      return {'cpu_count': cpu_count, 'ram_mb': ram_mb}

    kvm_pid_1, info_list = self.getKvmProcessInfo(['-smp', '-m'])
    self.assertEqual(
      ['2,maxcpus=3', '4096M,slots=128,maxmem=4608M'],
      info_list
    )
    self.assertEqual(
      getHotpluggedCpuRamValue(),
      {'cpu_count': 0, 'ram_mb': 0}
    )

    parameter_dict = {
      'enable-device-hotplug': 'true',
      # to avoid restarts the max RAM and CPU has to be static
      'ram-max-size': '8192',
      'cpu-max-count': '6',
    }
    self.rerequestInstance(parameter_dict)
    self.slap.waitForInstance(max_retry=2)
    kvm_pid_2, info_list = self.getKvmProcessInfo(['-smp', '-m'])

    self.assertEqual(
      ['2,maxcpus=6', '4096M,slots=128,maxmem=8192M'],
      info_list
    )
    self.assertEqual(
      getHotpluggedCpuRamValue(),
      {'cpu_count': 0, 'ram_mb': 0}
    )
    self.assertNotEqual(kvm_pid_1, kvm_pid_2, "Unexpected: KVM not restarted")
    parameter_dict.update(**{
      'ram-size': '5120',
      'cpu-count': '4'
    })
    self.rerequestInstance(parameter_dict)
    self.slap.waitForInstance(max_retry=10)
    kvm_pid_3, info_list = self.getKvmProcessInfo(['-smp', '-m'])

    self.assertEqual(
      ['2,maxcpus=6', '4096M,slots=128,maxmem=8192M'],
      info_list
    )
    self.assertEqual(kvm_pid_2, kvm_pid_3, "Unexpected: KVM restarted")
    self.assertEqual(
      getHotpluggedCpuRamValue(),
      {'cpu_count': 2, 'ram_mb': 1024}
    )


@skipUnlessKvm
class TestMemoryManagementJson(KvmMixinJson, TestMemoryManagement):
  pass


class MonitorAccessMixin(KvmMixin):
  def sqlite3_connect(self):
    sqlitedb_file = os.path.join(
      os.path.abspath(
        os.path.join(
          self.slap.instance_directory, os.pardir
        )
      ), 'var', 'proxy.db'
    )
    return sqlite3.connect(sqlitedb_file)

  def get_all_instantiated_partition_list(self):
    db = self.sqlite3_connect()
    try:
      db.row_factory = lambda cursor, row: {
        col[0]: row[idx]
        for idx, col in enumerate(cursor.description)
      }
      return db.execute(
        "SELECT reference, xml, connection_xml, partition_reference,"
        " software_release, requested_state, software_type"
        " FROM partition%s"
        " WHERE slap_state='busy'" % DB_VERSION).fetchall()
    finally:
      db.close()

  def test_access_monitor(self):
    connection_parameter_dict = self.getConnectionParameterDictJson()
    monitor_setup_url = connection_parameter_dict['monitor-setup-url']
    monitor_url_with_auth = 'https' + monitor_setup_url.split('https')[2]

    auth = parse_qs(urlparse(monitor_url_with_auth).path)

    # check that monitor-base-url for all partitions in the tree are accessible
    # with published username and password
    partition_with_monitor_base_url_count = 0
    for partition_information in self.get_all_instantiated_partition_list():
      connection_xml = partition_information.get('connection_xml')
      if not connection_xml:
        continue
      connection_dict = json.loads(slapos.util.xml2dict(connection_xml)['_'])
      monitor_base_url = connection_dict.get('monitor-base-url')
      if not monitor_base_url:
        continue
      result = requests.get(
        monitor_base_url, verify=False, auth=(
          auth['username'][0],
          auth['password'][0])
      )

      self.assertEqual(
        httplib.OK,
        result.status_code
      )
      partition_with_monitor_base_url_count += 1
    self.assertEqual(
      self.expected_partition_with_monitor_base_url_count,
      partition_with_monitor_base_url_count
    )


@skipUnlessKvm
class TestAccessDefault(MonitorAccessMixin, InstanceTestCase):
  __partition_reference__ = 'ad'
  expected_partition_with_monitor_base_url_count = 1

  def test(self):
    connection_parameter_dict = self.getConnectionParameterDictJson()
    result = requests.get(connection_parameter_dict['url'], verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>noVNC</title>', result.text)
    self.assertNotIn('url-additional', connection_parameter_dict)


@skipUnlessKvm
class TestAccessDefaultJson(KvmMixinJson, TestAccessDefault):
  pass


@skipUnlessKvm
class TestAccessDefaultAdditional(MonitorAccessMixin, InstanceTestCase):
  __partition_reference__ = 'ada'
  expected_partition_with_monitor_base_url_count = 1

  @classmethod
  def getInstanceParameterDict(cls):
    return {
      'frontend-additional-instance-guid': 'SOMETHING'
    }

  def test(self):
    connection_parameter_dict = self.getConnectionParameterDictJson()

    result = requests.get(connection_parameter_dict['url'], verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>noVNC</title>', result.text)

    result = requests.get(
      connection_parameter_dict['url-additional'], verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>noVNC</title>', result.text)


@skipUnlessKvm
class TestAccessDefaultAdditionalJson(
  KvmMixinJson, TestAccessDefaultAdditional):
  pass


@skipUnlessKvm
class TestAccessDefaultBootstrap(MonitorAccessMixin, InstanceTestCase):
  __partition_reference__ = 'adb'
  expected_partition_with_monitor_base_url_count = 1

  @classmethod
  def getInstanceParameterDict(cls):
    return {'_': json.dumps(dict(
      bootstrap_common_param_dict, **bootstrap_machine_param_dict))}

  def test(self):
    # START: mock .slapos-resource with tap.ipv4_addr
    # needed for netconfig.sh
    test_partition_slapos_resource_file = os.path.join(
      self.computer_partition_root_path, '.slapos-resource')
    path = os.path.realpath(os.curdir)
    while path != '/':
      root_slapos_resource_file = os.path.join(path, '.slapos-resource')
      if os.path.exists(root_slapos_resource_file):
        break
      path = os.path.realpath(os.path.join(path, '..'))
    else:
      raise ValueError('No .slapos-resource found to base the mock on')
    with open(root_slapos_resource_file) as fh:
      root_slapos_resource = json.load(fh)
    if root_slapos_resource['tap']['ipv4_addr'] == '':
      root_slapos_resource['tap'].update({
        "ipv4_addr": "10.0.0.2",
        "ipv4_gateway": "10.0.0.1",
        "ipv4_netmask": "255.255.0.0",
        "ipv4_network": "10.0.0.0"
      })
    with open(test_partition_slapos_resource_file, 'w') as fh:
      json.dump(root_slapos_resource, fh, indent=4)
    self.slap.waitForInstance(max_retry=10)
    # END: mock .slapos-resource with tap.ipv4_addr

    connection_parameter_dict = self.getConnectionParameterDictJson()

    result = requests.get(connection_parameter_dict['url'], verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>noVNC</title>', result.text)
    # check that expected files to configure the VM are exposed by the instance
    self.assertEqual(
      ['delDefaultIface', 'netconfig.sh'],
      sorted(os.listdir(os.path.join(
        self.computer_partition_root_path, 'srv', 'public')))
    )


@skipUnlessKvm
class TestAccessKvmCluster(MonitorAccessMixin, InstanceTestCase):
  __partition_reference__ = 'akc'
  expected_partition_with_monitor_base_url_count = 2

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-cluster'

  @classmethod
  def getInstanceParameterDict(cls):
    return {'_': json.dumps({
      "kvm-partition-dict": {
        "KVM0": {
            "disable-ansible-promise": True
        }
      }
    })}

  def test(self):
    connection_parameter_dict = self.getConnectionParameterDictJson()
    result = requests.get(connection_parameter_dict['KVM0-url'], verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>noVNC</title>', result.text)
    self.assertNotIn('KVM0-url-additional', connection_parameter_dict)


@skipUnlessKvm
class TestAccessKvmClusterAdditional(MonitorAccessMixin, InstanceTestCase):
  __partition_reference__ = 'akca'
  expected_partition_with_monitor_base_url_count = 2

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-cluster'

  @classmethod
  def getInstanceParameterDict(cls):
    return {'_': json.dumps({
      "frontend": {
        'frontend-additional-instance-guid': 'SOMETHING',
      },
      "kvm-partition-dict": {
        "KVM0": {
            "disable-ansible-promise": True,
        }
      }
    })}

  def test(self):
    connection_parameter_dict = self.getConnectionParameterDictJson()
    result = requests.get(connection_parameter_dict['KVM0-url'], verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>noVNC</title>', result.text)

    result = requests.get(
      connection_parameter_dict['KVM0-url-additional'], verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>noVNC</title>', result.text)


@skipUnlessKvm
class TestAccessKvmClusterBootstrap(MonitorAccessMixin, InstanceTestCase):
  __partition_reference__ = 'akcb'
  expected_partition_with_monitor_base_url_count = 3

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-cluster'

  @classmethod
  def getInstanceParameterDict(cls):
    return {'_': json.dumps(dict(bootstrap_common_param_dict, **{
      "kvm-partition-dict": {
          "test-machine1": bootstrap_machine_param_dict,
          "test-machine2": dict(bootstrap_machine_param_dict, **{
              "virtual-hard-drive-url":
              "http://shacache.org/shacache/5bdc95ea3f8ca40ff4fb8d086776e393"
              "87a68e91f76b1a5f883dfc33fa13cf1ee71c7d218a4e9401f56519a352791"
              "272ada4a5c334b3ca38a32c0bcacb6838e2",
              "virtual-hard-drive-md5sum": "deaf751a31dd6aec320d67c75c88c2e1",
              "virtual-hard-drive-gzipped": True,
          })
      }
    }))}

  def test(self):
    connection_parameter_dict = self.getConnectionParameterDictJson()
    result = requests.get(
      connection_parameter_dict['test-machine1-url'], verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>noVNC</title>', result.text)
    result = requests.get(
      connection_parameter_dict['test-machine2-url'], verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>noVNC</title>', result.text)


@skipUnlessKvm
class TestInstanceResilient(InstanceTestCase, KvmMixin):
  __partition_reference__ = 'ir'
  instance_max_retry = 20

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-resilient'

  def test_kvm_exporter(self):
    exporter_partition = os.path.join(
      self.slap.instance_directory,
      self.__partition_reference__ + '2')
    backup_path = os.path.join(
      exporter_partition, 'srv', 'backup', 'kvm', 'virtual.qcow2.gz')
    exporter = os.path.join(exporter_partition, 'bin', 'exporter')
    if os.path.exists(backup_path):
      os.unlink(backup_path)

    def call_exporter():
      try:
        return (0, subprocess.check_output(
          [exporter], stderr=subprocess.STDOUT).decode('utf-8'))
      except subprocess.CalledProcessError as e:
        return (e.returncode, e.output.decode('utf-8'))
    status_code, status_text = call_exporter()
    self.assertEqual(0, status_code, status_text)

  def test(self):
    connection_parameter_dict = self\
      .computer_partition.getConnectionParameterDict()
    present_key_list = []
    assert_key_list = [
     'monitor-password', 'takeover-kvm-1-password', 'backend-url', 'url',
     'monitor-setup-url', 'ipv6-network-info']
    for k in assert_key_list:
      if k in connection_parameter_dict:
        present_key_list.append(k)
        connection_parameter_dict.pop(k)
    self.assertIn('feed-url-kvm-1-pull', connection_parameter_dict)
    feed_pull = connection_parameter_dict.pop('feed-url-kvm-1-pull')
    self.assertRegex(
      feed_pull,
      'http://\\[{}\\]:[0-9][0-9][0-9][0-9]/get/local-ir0-kvm-1-pull'.format(
        self._ipv6_address))
    feed_push = connection_parameter_dict.pop('feed-url-kvm-1-push')
    self.assertRegex(
      feed_push,
      'http://\\[{}\\]:[0-9][0-9][0-9][0-9]/get/local-ir0-kvm-1-push'.format(
        self._ipv6_address))
    self.assertEqual(
      connection_parameter_dict,
      {
        'ipv6': self._ipv6_address,
        'monitor-base-url': f'https://[{self._ipv6_address}]:8160',
        'monitor-user': 'admin',
        'takeover-kvm-1-url': f'http://[{self._ipv6_address}]:9263/',
      }
    )
    self.assertEqual(set(present_key_list), set(assert_key_list))

    self.assertEqual(
      """ir0:bootstrap-monitor EXITED
ir0:certificate_authority-{hash}-on-watch RUNNING
ir0:crond-{hash}-on-watch RUNNING
ir0:monitor-httpd-{hash}-on-watch RUNNING
ir0:monitor-httpd-graceful EXITED
ir1:bootstrap-monitor EXITED
ir1:certificate_authority-{hash}-on-watch RUNNING
ir1:crond-{hash}-on-watch RUNNING
ir1:equeue-on-watch RUNNING
ir1:monitor-httpd-{hash}-on-watch RUNNING
ir1:monitor-httpd-graceful EXITED
ir1:notifier-on-watch RUNNING
ir1:pbs_sshkeys_authority-on-watch RUNNING
ir2:6tunnel-10022-{hash}-on-watch RUNNING
ir2:6tunnel-10080-{hash}-on-watch RUNNING
ir2:6tunnel-10443-{hash}-on-watch RUNNING
ir2:bootstrap-monitor EXITED
ir2:certificate_authority-{hash}-on-watch RUNNING
ir2:crond-{hash}-on-watch RUNNING
ir2:equeue-on-watch RUNNING
ir2:kvm-{kvm-hash-value}-on-watch RUNNING
ir2:kvm_controller EXITED
ir2:monitor-httpd-{hash}-on-watch RUNNING
ir2:monitor-httpd-graceful EXITED
ir2:nginx-graceful EXITED
ir2:nginx-on-watch RUNNING
ir2:notifier-on-watch RUNNING
ir2:resilient_sshkeys_authority-on-watch RUNNING
ir2:sshd-graceful EXITED
ir2:sshd-on-watch RUNNING
ir2:whitelist-domains-download-{hash} RUNNING
ir2:whitelist-firewall-{hash} RUNNING
ir3:bootstrap-monitor EXITED
ir3:certificate_authority-{hash}-on-watch RUNNING
ir3:crond-{hash}-on-watch RUNNING
ir3:equeue-on-watch RUNNING
ir3:monitor-httpd-{hash}-on-watch RUNNING
ir3:monitor-httpd-graceful EXITED
ir3:notifier-on-watch RUNNING
ir3:resilient-web-takeover-httpd-on-watch RUNNING
ir3:resilient_sshkeys_authority-on-watch RUNNING
ir3:sshd-graceful EXITED
ir3:sshd-on-watch RUNNING""",
      self.getProcessInfo()
    )


@skipUnlessKvm
class TestInstanceResilientJson(
  KvmMixinJson, TestInstanceResilient):
  pass


@skipUnlessKvm
class TestInstanceResilientDiskTypeIde(InstanceTestCase, KvmMixin):
  @classmethod
  def getInstanceParameterDict(cls):
    return {
      'disk-type': 'ide'
    }


@skipUnlessKvm
class TestInstanceResilientDiskTypeIdeJson(
  KvmMixinJson, TestInstanceResilientDiskTypeIde):
  pass


@skipUnlessKvm
class TestAccessResilientAdditional(InstanceTestCase):
  __partition_reference__ = 'ara'
  expected_partition_with_monitor_base_url_count = 1

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-resilient'

  @classmethod
  def getInstanceParameterDict(cls):
    return {
      'frontend-additional-instance-guid': 'SOMETHING'
    }

  def test(self):
    connection_parameter_dict = self.computer_partition\
      .getConnectionParameterDict()

    result = requests.get(connection_parameter_dict['url'], verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>noVNC</title>', result.text)

    result = requests.get(
      connection_parameter_dict['url-additional'], verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>noVNC</title>', result.text)


@skipUnlessKvm
class TestAccessResilientAdditionalJson(
  KvmMixinJson, TestAccessResilientAdditional):
  pass


class TestInstanceNbdServer(InstanceTestCase):
  __partition_reference__ = 'ins'
  instance_max_retry = 5

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'nbd'

  @classmethod
  def getInstanceParameterDict(cls):
    # port 8080 is used by testnode, use another one
    return {
      'otu-port': '8090'
    }

  def test(self):
    connection_parameter_dict = self.computer_partition\
      .getConnectionParameterDict()
    result = requests.get(
      connection_parameter_dict['upload_url'].strip(), verify=False)
    self.assertEqual(
      httplib.OK,
      result.status_code
    )
    self.assertIn('<title>Upload new File</title>', result.text)
    self.assertIn("WARNING", connection_parameter_dict['status_message'])


@skipUnlessKvm
class TestInstanceNbdServerJson(
  KvmMixinJson, TestInstanceNbdServer):
  pass


class HttpHandler(http.server.SimpleHTTPRequestHandler):
  def log_message(self, *args):
    if os.environ.get('SLAPOS_TEST_DEBUG'):
      return http.server.SimpleHTTPRequestHandler.log_message(self, *args)
    else:
      return


class FakeImageServerMixin(KvmMixin):
  @classmethod
  def startImageHttpServer(cls):
    cls.image_source_directory = tempfile.mkdtemp()
    server = SocketServer.TCPServer(
      (cls._ipv4_address, findFreeTCPPort(cls._ipv4_address)),
      HttpHandler)

    # c89f17758be13adeb06886ef935d5ff1
    fake_image_content = b'fake_image_content'
    cls.fake_image_md5sum = hashlib.md5(fake_image_content).hexdigest()
    with open(os.path.join(
      cls.image_source_directory, cls.fake_image_md5sum), 'wb') as fh:
      fh.write(fake_image_content)

    # bc81d2aee81e030c6cee210c802339c2
    fake_image2_content = b'fake_image2_content'
    cls.fake_image2_md5sum = hashlib.md5(fake_image2_content).hexdigest()
    with open(os.path.join(
      cls.image_source_directory, cls.fake_image2_md5sum), 'wb') as fh:
      fh.write(fake_image2_content)

    cls.fake_image_wrong_md5sum = cls.fake_image2_md5sum

    # c5ef5d70ad5a0dbfd890a734f588e344
    fake_image3_content = b'fake_image3_content'
    cls.fake_image3_md5sum = hashlib.md5(fake_image3_content).hexdigest()
    with open(os.path.join(
      cls.image_source_directory, cls.fake_image3_md5sum), 'wb') as fh:
      fh.write(fake_image3_content)

    url = 'http://%s:%s' % server.server_address
    cls.fake_image = '/'.join([url, cls.fake_image_md5sum])
    cls.fake_image2 = '/'.join([url, cls.fake_image2_md5sum])
    cls.fake_image3 = '/'.join([url, cls.fake_image3_md5sum])

    old_dir = os.path.realpath(os.curdir)
    os.chdir(cls.image_source_directory)
    try:
      cls.server_process = multiprocessing.Process(
        target=server.serve_forever, name='FakeImageHttpServer')
      cls.server_process.start()
      server.socket.close()
    finally:
      os.chdir(old_dir)

  @classmethod
  def stopImageHttpServer(cls):
    cls.logger.debug('Stopping process %s', cls.server_process)
    cls.server_process.join(10)
    cls.server_process.terminate()
    time.sleep(0.1)
    if cls.server_process.is_alive():
      cls.logger.warning(
        'Process %s still alive', cls.server_process)

    shutil.rmtree(cls.image_source_directory)


@skipUnlessKvm
class TestBootImageUrlList(InstanceTestCase, FakeImageServerMixin):
  __partition_reference__ = 'biul'
  kvm_instance_partition_reference = 'biul0'

  # variations
  key = 'boot-image-url-list'
  test_input = "%s#%s\n%s#%s"
  empty_input = ""
  image_directory = 'boot-image-url-list-repository'
  config_state_promise = 'boot-image-url-list-config-state-promise.py'
  download_md5sum_promise = 'boot-image-url-list-download-md5sum-promise.py'
  download_state_promise = 'boot-image-url-list-download-state-promise.py'

  bad_value = "jsutbad"
  incorrect_md5sum_value_image = "%s#"
  incorrect_md5sum_value = "url#asdasd"
  single_image_value = "%s#%s"
  unreachable_host_value = "evennotahost#%s"
  too_many_image_value = """
      image1#11111111111111111111111111111111
      image2#22222222222222222222222222222222
      image3#33333333333333333333333333333333
      image4#44444444444444444444444444444444
      image5#55555555555555555555555555555555
      image6#66666666666666666666666666666666
      """

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'default'

  @classmethod
  def getInstanceParameterDict(cls):
    return {
      cls.key: cls.test_input % (
        cls.fake_image, cls.fake_image_md5sum, cls.fake_image2,
        cls.fake_image2_md5sum)
    }

  @classmethod
  def setUpClass(cls):
    cls.startImageHttpServer()
    super().setUpClass()

  @classmethod
  def tearDownClass(cls):
    super().tearDownClass()
    cls.stopImageHttpServer()

  def tearDown(self):
    # clean up the instance for other tests
    # 1st remove all images...
    self.rerequestInstance({self.key: ''})
    self.slap.waitForInstance(max_retry=10)
    # 2nd ...move instance to "default" state
    self.rerequestInstance({})
    self.slap.waitForInstance(max_retry=10)
    super().tearDown()

  def getRunningImageList(
      self, kvm_instance_partition,
      _match_cdrom=re.compile('file=(.+),media=cdrom$').match,
      _sub_iso=re.compile(r'(/debian)(-[^-/]+)(-[^/]+-netinst\.iso)$').sub,
    ):
    with self.slap.instance_supervisor_rpc as instance_supervisor:
      kvm_pid = next(q for q in instance_supervisor.getAllProcessInfo()
                     if 'kvm-' in q['name'])['pid']
    sub_shared = re.compile(r'^%s/[^/]+/[0-9a-f]{32}/'
                            % re.escape(self.slap.shared_directory)).sub
    image_list = []
    for entry in psutil.Process(kvm_pid).cmdline():
      m = _match_cdrom(entry)
      if m:
        path = m.group(1)
        image_list.append(
          _sub_iso(
            r'\1-${ver}\3',
            sub_shared(
              r'${shared}/',
              path.replace(kvm_instance_partition, '${inst}')
            )))
    return image_list

  def test(self):
    # check that image is correctly downloaded
    kvm_instance_partition = os.path.join(
      self.slap.instance_directory, self.kvm_instance_partition_reference)
    image_repository = os.path.join(
      kvm_instance_partition, 'srv', self.image_directory)
    image = os.path.join(image_repository, self.fake_image_md5sum)
    self.assertTrue(os.path.exists(image))
    with open(image, 'rb') as fh:
      image_md5sum = hashlib.md5(fh.read()).hexdigest()
    self.assertEqual(image_md5sum, self.fake_image_md5sum)

    image2 = os.path.join(image_repository, self.fake_image2_md5sum)
    self.assertTrue(os.path.exists(image2))
    with open(image2, 'rb') as fh:
      image2_md5sum = hashlib.md5(fh.read()).hexdigest()
    self.assertEqual(image2_md5sum, self.fake_image2_md5sum)

    self.assertEqual(
      [
        f'${{inst}}/srv/{self.image_directory}/{self.fake_image_md5sum}',
        f'${{inst}}/srv/{self.image_directory}/{self.fake_image2_md5sum}',
        '${shared}/debian-${ver}-amd64-netinst.iso',
      ],
      self.getRunningImageList(kvm_instance_partition)
    )

    # Switch image
    self.rerequestInstance({
      self.key: self.test_input % (
        self.fake_image3, self.fake_image3_md5sum,
        self.fake_image2, self.fake_image2_md5sum)
    })
    self.slap.waitForInstance(max_retry=10)
    self.assertTrue(os.path.exists(os.path.join(
      image_repository, self.fake_image3_md5sum)))
    self.assertTrue(os.path.exists(os.path.join(
      image_repository, self.fake_image2_md5sum)))

    self.assertEqual(
      [
        f'${{inst}}/srv/{self.image_directory}/{self.fake_image3_md5sum}',
        f'${{inst}}/srv/{self.image_directory}/{self.fake_image2_md5sum}',
        '${shared}/debian-${ver}-amd64-netinst.iso',
      ],
      self.getRunningImageList(kvm_instance_partition)
    )

    # cleanup of images works, also asserts that configuration changes are
    # reflected
    # Note: key is left and empty_input is provided, as otherwise the part
    #       which generate images is simply removed, which can lead to
    #       leftover
    self.rerequestInstance({self.key: self.empty_input})
    self.slap.waitForInstance(max_retry=10)
    self.assertEqual(
      os.listdir(image_repository),
      []
    )

    # again only default image is available in the running process
    self.assertEqual(
      ['${shared}/debian-${ver}-amd64-netinst.iso'],
      self.getRunningImageList(kvm_instance_partition)
    )

  def assertPromiseFails(self, promise):
    partition_directory = os.path.join(
      self.slap.instance_directory,
      self.kvm_instance_partition_reference)
    monitor_run_promise = os.path.join(
      partition_directory, 'software_release', 'bin',
      'monitor.runpromise'
    )
    monitor_configuration = os.path.join(
      partition_directory, 'etc', 'monitor.conf')

    self.assertNotEqual(
      0,
      subprocess.call([
        monitor_run_promise, '-c', monitor_configuration, '-a', '-f',
        '--run-only', promise])
    )

  def test_bad_parameter(self):
    self.rerequestInstance({
      self.key: self.bad_value
    })
    self.raising_waitForInstance(3)
    self.assertPromiseFails(self.config_state_promise)

  def test_incorrect_md5sum(self):
    self.rerequestInstance({
      self.key: self.incorrect_md5sum_value_image % (self.fake_image,)
    })
    self.raising_waitForInstance(3)
    self.assertPromiseFails(self.config_state_promise)
    self.rerequestInstance({
      self.key: self.incorrect_md5sum_value
    })
    self.raising_waitForInstance(3)
    self.assertPromiseFails(self.config_state_promise)

  def test_not_matching_md5sum(self):
    self.rerequestInstance({
      self.key: self.single_image_value % (
        self.fake_image, self.fake_image_wrong_md5sum)
    })
    self.raising_waitForInstance(3)
    self.assertPromiseFails(self.download_md5sum_promise)
    self.assertPromiseFails(self.download_state_promise)

  def test_unreachable_host(self):
    self.rerequestInstance({
      self.key: self.unreachable_host_value % (
        self.fake_image_md5sum,)
    })
    self.raising_waitForInstance(3)
    self.assertPromiseFails(self.download_state_promise)

  def test_too_many_images(self):
    self.rerequestInstance({
      self.key: self.too_many_image_value
    })
    self.raising_waitForInstance(3)
    self.assertPromiseFails(self.config_state_promise)


@skipUnlessKvm
class TestBootImageUrlListJson(
  KvmMixinJson, TestBootImageUrlList):
  pass


@skipUnlessKvm
class TestBootImageUrlListResilient(TestBootImageUrlList):
  kvm_instance_partition_reference = 'biul2'

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-resilient'


@skipUnlessKvm
class TestBootImageUrlListResilientJson(
  KvmMixinJson, TestBootImageUrlListResilient):
  pass


@skipUnlessKvm
class TestBootImageUrlSelect(TestBootImageUrlList):
  __partition_reference__ = 'bius'
  kvm_instance_partition_reference = 'bius0'

  # variations
  key = 'boot-image-url-select'
  test_input = '["%s#%s", "%s#%s"]'
  empty_input = '[]'
  image_directory = 'boot-image-url-select-repository'
  config_state_promise = 'boot-image-url-select-config-state-promise.py'
  download_md5sum_promise = 'boot-image-url-select-download-md5sum-promise.py'
  download_state_promise = 'boot-image-url-select-download-state-promise.py'

  bad_value = '["jsutbad"]'
  incorrect_md5sum_value_image = '["%s#"]'
  incorrect_md5sum_value = '["url#asdasd"]'
  single_image_value = '["%s#%s"]'
  unreachable_host_value = '["evennotahost#%s"]'
  too_many_image_value = """[
      "image1#11111111111111111111111111111111",
      "image2#22222222222222222222222222222222",
      "image3#33333333333333333333333333333333",
      "image4#44444444444444444444444444444444",
      "image5#55555555555555555555555555555555",
      "image6#66666666666666666666666666666666"
      ]"""

  def test_not_json(self):
    self.rerequestInstance({
      self.key: 'notjson#notjson'
    })
    self.raising_waitForInstance(3)
    self.assertPromiseFails(self.config_state_promise)

  def test_together(self):
    partition_parameter_kw = {
      'boot-image-url-list': "{}#{}".format(
        self.fake_image, self.fake_image_md5sum),
      'boot-image-url-select': '["{}#{}"]'.format(
        self.fake_image, self.fake_image_md5sum)
    }
    self.rerequestInstance(partition_parameter_kw)
    self.slap.waitForInstance(max_retry=10)
    # check that image is correctly downloaded
    for image_directory in [
      'boot-image-url-list-repository', 'boot-image-url-select-repository']:
      image_repository = os.path.join(
        self.slap.instance_directory, self.kvm_instance_partition_reference,
        'srv', image_directory)
      image = os.path.join(image_repository, self.fake_image_md5sum)
      self.assertTrue(os.path.exists(image))
      with open(image, 'rb') as fh:
        image_md5sum = hashlib.md5(fh.read()).hexdigest()
      self.assertEqual(image_md5sum, self.fake_image_md5sum)

    kvm_instance_partition = os.path.join(
      self.slap.instance_directory, self.kvm_instance_partition_reference)

    self.assertEqual(
      [
        '${{inst}}/srv/boot-image-url-select-repository/{}'.format(
          self.fake_image_md5sum),
        '${{inst}}/srv/boot-image-url-list-repository/{}'.format(
          self.fake_image_md5sum),
        '${shared}/debian-${ver}-amd64-netinst.iso',
      ],
      self.getRunningImageList(kvm_instance_partition)
    )

    # cleanup of images works, also asserts that configuration changes are
    # reflected
    self.rerequestInstance(
      {'boot-image-url-list': '', 'boot-image-url-select': ''})
    self.slap.waitForInstance(max_retry=2)
    for image_directory in [
      'boot-image-url-list-repository', 'boot-image-url-select-repository']:
      image_repository = os.path.join(
        kvm_instance_partition, 'srv', image_directory)
      self.assertEqual(
        os.listdir(image_repository),
        []
      )

    # cleanup of images works, also asserts that configuration changes are
    # reflected
    partition_parameter_kw[self.key] = ''
    partition_parameter_kw['boot-image-url-list'] = ''
    self.rerequestInstance(partition_parameter_kw)
    self.slap.waitForInstance(max_retry=2)
    self.assertEqual(
      os.listdir(image_repository),
      []
    )

    # again only default image is available in the running process
    self.assertEqual(
      ['${shared}/debian-${ver}-amd64-netinst.iso'],
      self.getRunningImageList(kvm_instance_partition)
    )


@skipUnlessKvm
class TestBootImageUrlSelectJson(
  KvmMixinJson, TestBootImageUrlSelect):
  pass


@skipUnlessKvm
class TestBootImageUrlSelectResilient(TestBootImageUrlSelect):
  kvm_instance_partition_reference = 'bius2'

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-resilient'


@skipUnlessKvm
class TestBootImageUrlSelectResilientJson(
  KvmMixinJson, TestBootImageUrlSelectResilient):
  pass


@skipUnlessKvm
class TestBootImageUrlListKvmCluster(InstanceTestCase, FakeImageServerMixin):
  __partition_reference__ = 'biulkc'

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-cluster'

  input_value = "%s#%s"
  key = 'boot-image-url-list'
  config_file_name = 'boot-image-url-list.conf'

  def setUp(self):
    super().setUp()
    self.startImageHttpServer()

  def tearDown(self):
    self.stopImageHttpServer()
    super().tearDown()

  @classmethod
  def getInstanceParameterDict(cls):
    return {'_': json.dumps({
      "kvm-partition-dict": {
        "KVM0": {
            "disable-ansible-promise": True,
        },
        "KVM1": {
            "disable-ansible-promise": True,
        }
      }
    })}

  def test(self):
    # Note: As there is no way to introspect nicely where partition landed
    #       we assume ordering of the cluster requests
    self.rerequestInstance({'_': json.dumps({
      "kvm-partition-dict": {
        "KVM0": {
            "disable-ansible-promise": True,
            self.key: self.input_value % (
              self.fake_image, self.fake_image_md5sum)
        },
        "KVM1": {
            "disable-ansible-promise": True,
            self.key: self.input_value % (
              self.fake_image2, self.fake_image2_md5sum)
        }
      }
    })})
    self.slap.waitForInstance(max_retry=10)
    KVM0_config = os.path.join(
      self.slap.instance_directory, self.__partition_reference__ + '1', 'etc',
      self.config_file_name)
    KVM1_config = os.path.join(
      self.slap.instance_directory, self.__partition_reference__ + '2', 'etc',
      self.config_file_name)
    with open(KVM0_config) as fh:
      self.assertEqual(
        self.input_value % (self.fake_image, self.fake_image_md5sum),
        fh.read().strip()
      )
    with open(KVM1_config) as fh:
      self.assertEqual(
        self.input_value % (self.fake_image2, self.fake_image2_md5sum),
        fh.read().strip()
      )


@skipUnlessKvm
class TestBootImageUrlSelectKvmCluster(TestBootImageUrlListKvmCluster):
  __partition_reference__ = 'biuskc'

  input_value = "[\"%s#%s\"]"
  key = 'boot-image-url-select'
  config_file_name = 'boot-image-url-select.json'


@skipUnlessKvm
class TestNatRules(KvmMixin, InstanceTestCase):
  __partition_reference__ = 'nr'

  @classmethod
  def getInstanceParameterDict(cls):
    return {
      'nat-rules': '100 200',
    }

  def test(self):
    connection_parameter_dict = self.getConnectionParameterDictJson()

    self.assertIn('nat-rule-port-tcp-100', connection_parameter_dict)
    self.assertIn('nat-rule-port-tcp-200', connection_parameter_dict)

    self.assertEqual(
      f'{self._ipv6_address} : 10100',
      connection_parameter_dict['nat-rule-port-tcp-100']
    )
    self.assertEqual(
      f'{self._ipv6_address} : 10200',
      connection_parameter_dict['nat-rule-port-tcp-200']
    )


@skipUnlessKvm
class TestNatRulesJson(
  KvmMixinJson, TestNatRules):
  pass


@skipUnlessKvm
class TestNatRulesKvmCluster(InstanceTestCase):
  __partition_reference__ = 'nrkc'

  nat_rules = ["100", "200", "300"]

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-cluster'

  @classmethod
  def getInstanceParameterDict(cls):
    return {'_': json.dumps({
      "kvm-partition-dict": {
        "KVM0": {
            "nat-rules": cls.nat_rules,
            "disable-ansible-promise": True,
        }
      }
    })}

  def getRunningHostFwd(self):
    with self.slap.instance_supervisor_rpc as instance_supervisor:
      kvm_pid = [q for q in instance_supervisor.getAllProcessInfo()
                 if 'kvm-' in q['name']][0]['pid']
      kvm_process = psutil.Process(kvm_pid)
      for entry in kvm_process.cmdline():
        if 'hostfwd' in entry:
          return entry

  def test(self):
    host_fwd_entry = self.getRunningHostFwd()
    self.assertIn(
      f'hostfwd=tcp:{self._ipv4_address}:10100-:100',
      host_fwd_entry)
    self.assertIn(
      f'hostfwd=tcp:{self._ipv4_address}:10200-:200',
      host_fwd_entry)
    self.assertIn(
      f'hostfwd=tcp:{self._ipv4_address}:10300-:300',
      host_fwd_entry)


@skipUnlessKvm
class TestNatRulesKvmClusterComplex(TestNatRulesKvmCluster):
  __partition_reference__ = 'nrkcc'
  nat_rules = ["100", "200 300"]


@skipUnlessKvm
class TestWhitelistFirewall(InstanceTestCase):
  __partition_reference__ = 'wf'
  kvm_instance_partition_reference = 'wf0'

  def test(self):
    slapos_whitelist_firewall = os.path.join(
      self.slap.instance_directory, self.kvm_instance_partition_reference,
      '.slapos-whitelist-firewall')
    self.assertTrue(os.path.exists(slapos_whitelist_firewall))
    with open(slapos_whitelist_firewall, 'rb') as fh:
      content = fh.read()
    try:
      self.content_json = json.loads(content)
    except ValueError:
      self.fail(f'Failed to parse json of {content!r}')
    self.assertTrue(isinstance(self.content_json, list))
    # check /etc/resolv.conf
    with open('/etc/resolv.conf') as f:
      resolv_conf_ip_list = []
      for line in f.readlines():
        if line.startswith('nameserver'):
          resolv_conf_ip_list.append(line.split()[1])
    resolv_conf_ip_list = list(set(resolv_conf_ip_list))
    self.assertFalse(len(resolv_conf_ip_list) == 0)
    self.assertTrue(all([q in self.content_json for q in resolv_conf_ip_list]))
    # there is something more
    self.assertGreater(len(self.content_json), len(resolv_conf_ip_list))


@skipUnlessKvm
class TestWhitelistFirewallJson(
  KvmMixinJson, TestWhitelistFirewall):
  pass


@skipUnlessKvm
class TestWhitelistFirewallRequest(TestWhitelistFirewall):
  whitelist_domains = '2.2.2.2 3.3.3.3\n4.4.4.4'

  @classmethod
  def getInstanceParameterDict(cls):
    return {
      'whitelist-domains': cls.whitelist_domains,
    }

  def test(self):
    super().test()
    self.assertIn('2.2.2.2', self.content_json)
    self.assertIn('3.3.3.3', self.content_json)
    self.assertIn('4.4.4.4', self.content_json)


@skipUnlessKvm
class TestWhitelistFirewallRequestJson(
  KvmMixinJson, TestWhitelistFirewallRequest):
  pass


@skipUnlessKvm
class TestWhitelistFirewallResilient(TestWhitelistFirewall):
  kvm_instance_partition_reference = 'wf2'

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-resilient'


@skipUnlessKvm
class TestWhitelistFirewallResilientJson(
  KvmMixinJson, TestWhitelistFirewallResilient):
  pass


@skipUnlessKvm
class TestWhitelistFirewallRequestResilient(TestWhitelistFirewallRequest):
  kvm_instance_partition_reference = 'wf2'

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-resilient'


@skipUnlessKvm
class TestWhitelistFirewallRequestResilientJson(
  KvmMixinJson, TestWhitelistFirewallRequestResilient):
  pass


@skipUnlessKvm
class TestWhitelistFirewallCluster(TestWhitelistFirewall):
  kvm_instance_partition_reference = 'wf1'

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-cluster'

  @classmethod
  def getInstanceParameterDict(cls):
    return {'_': json.dumps({
      "kvm-partition-dict": {
        "KVM0": {
            "disable-ansible-promise": True
        }
      }
    })}


@skipUnlessKvm
class TestWhitelistFirewallRequestCluster(TestWhitelistFirewallRequest):
  kvm_instance_partition_reference = 'wf1'

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-cluster'

  @classmethod
  def getInstanceParameterDict(cls):
    return {'_': json.dumps({
      "kvm-partition-dict": {
        "KVM0": {
            "whitelist-domains": cls.whitelist_domains,
            "disable-ansible-promise": True
        }
      }
    })}


@skipUnlessKvm
class TestDiskDevicePathWipeDiskOndestroy(InstanceTestCase, KvmMixin):
  __partition_reference__ = 'ddpwdo'
  kvm_instance_partition_reference = 'ddpwdo0'

  def test(self):
    self.rerequestInstance({
      'disk-device-path': '/dev/virt0 /dev/virt1',
      'wipe-disk-ondestroy': True
    })
    self.raising_waitForInstance(3)
    instance_path = os.path.join(
      self.slap.instance_directory, self.kvm_instance_partition_reference)

    slapos_wipe_device_disk = os.path.join(
      instance_path, 'etc', 'prerm', 'slapos_wipe_device_disk')

    # check prerm script, it's trusted that prerm manager really works
    self.assertTrue(os.path.exists(slapos_wipe_device_disk))
    with open(slapos_wipe_device_disk) as fh:
      self.assertEqual(
        fh.read().strip(),
        r"""#!/bin/sh
dd if=/dev/zero of=/dev/virt0 bs=4096 count=500k
dd if=/dev/zero of=/dev/virt1 bs=4096 count=500k"""
      )
    self.assertTrue(os.access(slapos_wipe_device_disk, os.X_OK))


@skipUnlessKvm
class TestDiskDevicePathWipeDiskOndestroyJson(
  KvmMixinJson, TestDiskDevicePathWipeDiskOndestroy):
  pass


@skipUnlessKvm
class TestImageDownloadController(InstanceTestCase, FakeImageServerMixin):
  __partition_reference__ = 'idc'
  maxDiff = None

  def setUp(self):
    super().setUp()
    self.working_directory = tempfile.mkdtemp()
    self.destination_directory = os.path.join(
      self.working_directory, 'destination')
    os.mkdir(self.destination_directory)
    self.config_json = os.path.join(
      self.working_directory, 'config.json')
    self.md5sum_fail_file = os.path.join(
      self.working_directory, 'md5sum_fail_file')
    self.error_state_file = os.path.join(
      self.working_directory, 'error_state_file')
    self.processed_md5sum = os.path.join(
      self.working_directory, 'processed_md5sum')
    self.startImageHttpServer()
    self.image_download_controller = os.path.join(
      self.slap.instance_directory, self.__partition_reference__ + '0',
      'software_release', 'parts', 'image-download-controller',
      'image-download-controller.py')

  def tearDown(self):
    self.stopImageHttpServer()
    shutil.rmtree(self.working_directory)
    super().tearDown()

  def callImageDownloadController(self, *args):
    call_list = [sys.executable, self.image_download_controller] + list(args)
    try:
      return (0, subprocess.check_output(
        call_list, stderr=subprocess.STDOUT).decode('utf-8'))
    except subprocess.CalledProcessError as e:
      return (e.returncode, e.output.decode('utf-8'))

  def runImageDownloadControlerWithDict(self, json_dict):
    with open(self.config_json, 'w') as fh:
      json.dump(json_dict, fh, indent=2)
    return self.callImageDownloadController(
      self.config_json,
      'curl',  # comes from test environemnt, considered to be recent enough
      self.md5sum_fail_file,
      self.error_state_file,
      self.processed_md5sum
    )

  def assertFileContent(self, path, content):
    self.assertTrue(os.path.exists, path)
    with open(path) as fh:
      self.assertEqual(
        fh.read(),
        content)

  def test(self):
    json_dict = {
      'error-amount': 0,
      'config-md5sum': 'config-md5sum',
      'destination-directory': self.destination_directory,
      'image-list': [
        {
          'destination-tmp': 'tmp',
          'url': self.fake_image,
          'destination': 'destination',
          'image-number': '001',
          'gzipped': False,
          'md5sum': self.fake_image_md5sum,
        }
      ]
    }
    code, result = self.runImageDownloadControlerWithDict(
      json_dict
    )
    self.assertEqual(
      (code, result.strip()),
      (0, """
INF: Storing errors in %(error_state_file)s
INF: %(fake_image)s : Downloading
INF: %(fake_image)s : Stored with checksum %(checksum)s
""".strip() % {
        'fake_image': self.fake_image,
        'checksum': self.fake_image_md5sum,
        'error_state_file': self.error_state_file,
        'destination': os.path.join(self.destination_directory, 'destination'),
      })
    )
    self.assertFileContent(self.md5sum_fail_file, '')
    self.assertFileContent(self.error_state_file, '')
    self.assertFileContent(self.processed_md5sum, 'config-md5sum')
    self.assertFalse(
      os.path.exists(os.path.join(self.destination_directory, 'tmp')))
    self.assertFileContent(
      os.path.join(self.destination_directory, 'destination'),
      'fake_image_content'
    )

    # Nothing happens if all is downloaded
    code, result = self.runImageDownloadControlerWithDict(
      json_dict
    )
    self.assertEqual(
      (code, result.strip()),
      (0, """
INF: Storing errors in %(error_state_file)s
INF: %(fake_image)s : already downloaded
""".strip() % {
        'fake_image': self.fake_image,
        'checksum': self.fake_image_md5sum,
        'error_state_file': self.error_state_file,
        'destination': os.path.join(self.destination_directory, 'destination'),
      })
    )

  def test_fail(self):
    json_dict = {
      'error-amount': 0,
      'config-md5sum': 'config-md5sum',
      'destination-directory': self.destination_directory,
      'image-list': [
        {
          'destination-tmp': 'tmp',
          'url': self.fake_image,
          'destination': 'destination',
          'image-number': '001',
          'gzipped': False,
          'md5sum': self.fake_image_wrong_md5sum,
        }
      ]
    }
    for try_num in range(1, 5):
      code, result = self.runImageDownloadControlerWithDict(
        json_dict
      )
      self.assertEqual(
        (code, result.strip()),
        (1, """
INF: Storing errors in %(error_state_file)s
INF: %(fake_image)s : Downloading
""".  strip() % {
          'fake_image': self.fake_image,
          'error_state_file': self.error_state_file,
          'destination': os.path.join(
            self.destination_directory, 'destination'),
        })
      )
      fake_image_url = '#'.join([
        self.fake_image, self.fake_image_wrong_md5sum])
      self.assertFileContent(
        self.md5sum_fail_file, """{
  "%s": %s
}""" % (fake_image_url, try_num))
      self.assertFileContent(
        self.error_state_file, """
        ERR: %(fake_image)s : MD5 mismatch expected is %(wrong_checksum)s """
        """but got instead %(real_checksum)s""".strip() % {
          'fake_image': self.fake_image,
          'wrong_checksum': self.fake_image_wrong_md5sum,
          'real_checksum': self.fake_image_md5sum,
        })
      self.assertFileContent(self.processed_md5sum, 'config-md5sum')
      self.assertFalse(
        os.path.exists(os.path.join(self.destination_directory, 'tmp')))
      self.assertFalse(
        os.path.exists(
          os.path.join(self.destination_directory, 'destination')))

    code, result = self.runImageDownloadControlerWithDict(
      json_dict
    )
    self.assertEqual(
      (code, result.strip()),
      (1, """
INF: Storing errors in %(error_state_file)s
""".  strip() % {
        'fake_image': self.fake_image,
        'error_state_file': self.error_state_file,
        'destination': os.path.join(
          self.destination_directory, 'destination'),
      })
    )
    fake_image_url = '#'.join([
      self.fake_image, self.fake_image_wrong_md5sum])
    self.assertFileContent(
      self.md5sum_fail_file, """{
  "%s": %s
}""" % (fake_image_url, 4))
    self.assertFileContent(
      self.error_state_file, """
      ERR: %(fake_image)s : Checksum is incorrect after 4 tries, will not """
      """retry""".strip() % {
        'fake_image': self.fake_image,
      })
    self.assertFileContent(self.processed_md5sum, 'config-md5sum')
    self.assertFalse(
      os.path.exists(os.path.join(self.destination_directory, 'tmp')))
    self.assertFalse(
      os.path.exists(
        os.path.join(self.destination_directory, 'destination')))


@skipUnlessKvm
class TestParameterDefault(InstanceTestCase, KvmMixin):
  __partition_reference__ = 'pd'

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'default'

  def mangleParameterDict(self, parameter_dict):
    return parameter_dict

  def _test(self, parameter_dict, expected):
    self.rerequestInstance(self.mangleParameterDict(parameter_dict))
    self.slap.waitForInstance(max_retry=10)

    kvm_raw = glob.glob(os.path.join(
      self.slap.instance_directory, '*', 'bin', 'kvm_raw'))
    self.assertEqual(len(kvm_raw), 1)
    kvm_raw = kvm_raw[0]
    with open(kvm_raw) as fh:
      kvm_raw = fh.read()
    self.assertIn(expected, kvm_raw)

  def test_disk_type_default(self):
    self._test({}, "disk_type = 'virtio'")

  def test_disk_type_set(self):
    self._test({'disk-type': 'ide'}, "disk_type = 'ide'")

  def test_network_adapter_default(self):
    self._test({}, "network_adapter = 'virtio-net-pci")

  def test_network_adapter_set(self):
    self._test({'network-adapter': 'e1000'}, "network_adapter = 'e1000'")

  def test_cpu_count_default(self):
    self._test({}, "init_smp_count = 2")

  def test_cpu_count_default_max(self):
    self._test({}, "smp_max_count = 3")

  def test_cpu_count_set(self):
    self._test({'cpu-count': 4}, "init_smp_count = 4")

  def test_cpu_count_set_max(self):
    self._test({'cpu-count': 4}, "smp_max_count = 5")

  def test_ram_size_default(self):
    self._test({}, "init_ram_size = 4096")

  def test_ram_size_default_max(self):
    self._test({}, "ram_max_size = '4608'")

  def test_ram_size_set(self):
    self._test({'ram-size': 2048}, "init_ram_size = 2048")

  def test_ram_size_set_max(self):
    self._test({'ram-size': 2048}, "ram_max_size = '2560'")


@skipUnlessKvm
class TestParameterDefaultJson(
  KvmMixinJson, TestParameterDefault):
  pass


@skipUnlessKvm
class TestParameterResilient(TestParameterDefault):
  __partition_reference__ = 'pr'

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-resilient'


@skipUnlessKvm
class TestParameterCluster(TestParameterDefault):
  __partition_reference__ = 'pc'

  parameter_dict = {
    "disable-ansible-promise": True
  }

  @classmethod
  def getInstanceParameterDict(cls):
    return {'_': json.dumps({
      "kvm-partition-dict": {
        "KVM0": cls.parameter_dict
      }
    })}

  def mangleParameterDict(self, parameter_dict):
    local_parameter_dict = self.parameter_dict.copy()
    local_parameter_dict.update(parameter_dict)
    return {'_': json.dumps({
      "kvm-partition-dict": {
        "KVM0": local_parameter_dict
      }
    })}

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-cluster'


class ExternalDiskMixin(KvmMixin):
  @classmethod
  def _prepareExternalStorageList(cls):
    external_storage_path = os.path.join(cls.working_directory, 'STORAGE')
    os.mkdir(external_storage_path)
    # reuse .slapos-resource infomration of the containing partition
    # it's similar to slapos/recipe/slapconfiguration.py
    _resource_home = cls.slap.instance_directory
    parent_slapos_resource = None
    while not os.path.exists(os.path.join(_resource_home, '.slapos-resource')):
      _resource_home = os.path.normpath(os.path.join(_resource_home, '..'))
      if _resource_home == "/":
        break
    else:
      with open(os.path.join(_resource_home, '.slapos-resource')) as fh:
        parent_slapos_resource = json.load(fh)
    assert parent_slapos_resource is not None

    for partition in os.listdir(cls.slap.instance_directory):
      if not partition.startswith(cls.__partition_reference__):
        continue
      partition_store_list = []
      for number in range(10):
        storage = os.path.join(external_storage_path, f'data{number}')
        if not os.path.exists(storage):
          os.mkdir(storage)
        partition_store = os.path.join(storage, partition)
        os.mkdir(partition_store)
        partition_store_list.append(partition_store)
      slapos_resource = parent_slapos_resource.copy()
      slapos_resource['external_storage_list'] = partition_store_list
      with open(
        os.path.join(
          cls.slap.instance_directory, partition, '.slapos-resource'),
        'w') as fh:
        json.dump(slapos_resource, fh, indent=2)
    # above is not enough: the presence of parameter is required in slapos.cfg
    slapos_config = []
    with open(cls.slap._slapos_config) as fh:
      for line in fh.readlines():
        if line.strip() == '[slapos]':
          slapos_config.append('[slapos]\n')
          slapos_config.append(
            f'instance_storage_home = {external_storage_path}\n')
        else:
          slapos_config.append(line)
    with open(cls.slap._slapos_config, 'w') as fh:
      fh.write(''.join(slapos_config))

  @classmethod
  def _dropExternalStorageList(cls):
    slapos_config = []
    with open(cls.slap._slapos_config) as fh:
      for line in fh.readlines():
        if line.startswith("instance_storage_home ="):
          continue
        slapos_config.append(line)
    with open(cls.slap._slapos_config, 'w') as fh:
      fh.write(''.join(slapos_config))

  def getRunningDriveList(self, kvm_instance_partition):
    _match_drive = re.compile('file.*if=virtio.*').match
    with self.slap.instance_supervisor_rpc as instance_supervisor:
      kvm_pid = next(q for q in instance_supervisor.getAllProcessInfo()
                     if 'kvm-' in q['name'])['pid']
    drive_list = []
    for entry in psutil.Process(kvm_pid).cmdline():
      m = _match_drive(entry)
      if m:
        path = m.group(0)
        drive_list.append(
          path.replace(kvm_instance_partition, '${partition}')
        )
    return drive_list


@skipUnlessKvm
class TestExternalDisk(InstanceTestCase, ExternalDiskMixin):
  __partition_reference__ = 'ed'
  kvm_instance_partition_reference = 'ed0'

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'default'

  @classmethod
  def getInstanceParameterDict(cls):
    return {
      'external-disk-number': 2,
      'external-disk-size': 1
    }

  @classmethod
  def _setUpClass(cls):
    super()._setUpClass()
    cls.working_directory = tempfile.mkdtemp()
    # setup the external_storage_list, to mimic part of slapformat
    cls._prepareExternalStorageList()
    # re-run the instance, as information has been updated
    cls.waitForInstance()

  @classmethod
  def tearDownClass(cls):
    cls._dropExternalStorageList()
    super().tearDownClass()
    shutil.rmtree(cls.working_directory)

  def test(self):
    kvm_instance_partition = os.path.join(
      self.slap.instance_directory, self.kvm_instance_partition_reference)
    drive_list = self.getRunningDriveList(kvm_instance_partition)

    # Note: Do to unknown set of drives it's impossible to directly check
    #       drive paths, thus the count is important
    self.assertEqual(
      1 + 2,  # 1 the default drive, 2 additional ones
      len(drive_list)
    )

    # restart the VM
    self.requestDefaultInstance(state='stopped')
    self.waitForInstance()
    self.requestDefaultInstance(state='started')
    self.waitForInstance()
    restarted_drive_list = self.getRunningDriveList(kvm_instance_partition)
    self.assertEqual(drive_list, restarted_drive_list)
    # prove that even on resetting parameters, drives are still there
    self.rerequestInstance({}, state='stopped')
    self.waitForInstance()
    self.rerequestInstance({})
    self.waitForInstance()
    dropped_drive_list = self.getRunningDriveList(kvm_instance_partition)
    self.assertEqual(drive_list, dropped_drive_list)


@skipUnlessKvm
class TestExternalDiskJson(
  KvmMixinJson, TestExternalDisk):
  pass


class ExternalDiskModernMixin(object):
  __partition_reference__ = 'edm'
  kvm_instance_partition_reference = 'edm0'

  @classmethod
  def setUpClass(cls):
    super().setUpClass()

  @classmethod
  def _setUpClass(cls):
    super()._setUpClass()
    cls.working_directory = tempfile.mkdtemp()
    # setup the external_storage_list, to mimic part of slapformat
    cls._prepareExternalStorageList()
    # re-run the instance, as information has been updated
    cls.waitForInstance()

  @classmethod
  def tearDownClass(cls):
    cls._dropExternalStorageList()
    super().tearDownClass()
    shutil.rmtree(cls.working_directory)

  def getExternalDiskInstanceParameterDict(
    self, first, second, third, update_dict=None):
    parameter_dict = {
      "external-disk": {
          "second disk": {
              "path": second,
              "index": 2,
          },
          "third disk": {
              "path": third,
              "index": 3,
              "cache": "none"
          },
          "first disk": {
              "path": first,
              "index": 1,
              "format": "qcow"
          },
      }
    }
    if update_dict is not None:
      parameter_dict.update(update_dict)
    return parameter_dict

  def prepareEnv(self):
    # Disks can't be created in /tmp, as it's specially mounted on testnodes
    # and then KVM can't use them:
    # -drive file=/tmp/tmpX/third_disk,if=virtio,cache=none: Could not open
    # '/tmp/tmpX/third_disk': filesystem does not support O_DIRECT
    self.working_directory = tempfile.mkdtemp(dir=self.slap.instance_directory)
    self.addCleanup(shutil.rmtree, self.working_directory)
    self.kvm_instance_partition = os.path.join(
      self.slap.instance_directory, self.kvm_instance_partition_reference)
    # find qemu_img from the tested SR via it's partition parameter, as
    # otherwise qemu-kvm would be dependency of test suite
    with open(
      os.path.join(self.computer_partition_root_path, 'buildout.cfg')) as fh:
      self.qemu_img = [
        q for q in fh.readlines()
        if 'raw qemu_img_executable_location' in q][0].split()[-1]
    self.first_disk = os.path.join(self.working_directory, 'first_disk')
    subprocess.check_call([
      self.qemu_img, "create", "-f", "qcow", self.first_disk, "1M"])
    self.second_disk_name = 'second_disk'
    self.second_disk = os.path.join(
      self.kvm_instance_partition, self.second_disk_name)
    subprocess.check_call([
      self.qemu_img, "create", "-f", "qcow2", os.path.join(
        self.kvm_instance_partition, self.second_disk), "1M"])
    self.third_disk = os.path.join(self.working_directory, 'third_disk')
    subprocess.check_call([
      self.qemu_img, "create", "-f", "qcow2", self.third_disk, "1M"])
    self.rerequestInstance({'_': json.dumps(
        self.getExternalDiskInstanceParameterDict(
          self.first_disk, self.second_disk_name, self.third_disk))})

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'default'


@skipUnlessKvm
class TestExternalDiskModern(
  ExternalDiskModernMixin, InstanceTestCase, ExternalDiskMixin):
  def test(self):
    self.prepareEnv()
    self.waitForInstance()
    drive_list = self.getRunningDriveList(self.kvm_instance_partition)
    self.assertEqual(
      drive_list,
      [
        'file=${partition}/srv/virtual.qcow2,if=virtio,discard=on,'
        'format=qcow2',
        'file={}/first_disk,if=virtio,cache=writeback,format=qcow'.format(
          self.working_directory),
        'file=${partition}/second_disk,if=virtio,cache=writeback',
        'file={}/third_disk,if=virtio,cache=none'.format(
          self.working_directory)
      ]
    )


@skipUnlessKvm
class TestExternalDiskModernConflictAssurance(
  ExternalDiskModernMixin, InstanceTestCase, ExternalDiskMixin):
  def test(self):
    self.prepareEnv()
    # Create conflicting configuration
    parameter_dict = {
      "external-disk-number": 1,
      "external-disk-size": 10,
      "external-disk-format": "qcow2",
    }
    self.rerequestInstance({'_': json.dumps(parameter_dict)})
    self.waitForInstance()
    data_disk_ids = os.path.join(
      self.kvm_instance_partition, 'etc', '.data-disk-ids')
    data_disk_amount = os.path.join(
      self.kvm_instance_partition, 'etc', '.data-disk-amount')
    self.assertTrue(os.path.exists(data_disk_ids))
    self.assertTrue(os.path.exists(data_disk_amount))
    with open(data_disk_amount) as fh:
      self.assertEqual(1, int(fh.read()))
    parameter_dict.update(self.getExternalDiskInstanceParameterDict(
      self.first_disk, self.second_disk_name, self.third_disk))
    parameter_dict["external-disk-number"] = 0
    # assert mutual exclusivity of old and modern
    self.rerequestInstance({'_': json.dumps(parameter_dict)})
    self.raising_waitForInstance(3)
    # Fix the situation
    with open(data_disk_amount, 'w') as fh:
      fh.write("0")
    self.waitForInstance()


@skipUnlessKvm
class TestExternalDiskModernCluster(TestExternalDiskModern):
  kvm_instance_partition_reference = 'edm1'

  @classmethod
  def getInstanceParameterDict(cls):
    return {'_': json.dumps({
      "kvm-partition-dict": {
        "kvm-default": {
            "disable-ansible-promise": True,
        }
      }
    })}

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'kvm-cluster'

  def getExternalDiskInstanceParameterDict(self, *args, **kwargs):
    partition_dict = super().getExternalDiskInstanceParameterDict(*args, **kwargs)
    partition_dict.update({"disable-ansible-promise": True})
    return {
      "kvm-partition-dict": {
        "kvm-default": partition_dict
      }
    }


@skipUnlessKvm
class TestExternalDiskModernIndexRequired(InstanceTestCase, ExternalDiskMixin):
  __partition_reference__ = 'edm'
  kvm_instance_partition_reference = 'edm0'

  @classmethod
  def getInstanceSoftwareType(cls):
    return 'default'

  @classmethod
  def setUpClass(cls):
    super().setUpClass()

  def getExternalDiskInstanceParameterDict(self, first, second, third):
    return {
      "external-disk": {
          "second disk": {
              "path": second,
          },
          "third disk": {
              "path": third,
              "index": 3,
          },
          "first disk": {
              "path": first,
              "index": 1,
          },
      }
    }

  def test(self):
    # Disks can't be created in /tmp, as it's specially mounted on testnodes
    # and then KVM can't use them:
    # -drive file=/tmp/tmpX/third_disk,if=virtio,cache=none: Could not open
    # '/tmp/tmpX/third_disk': filesystem does not support O_DIRECT
    self.working_directory = tempfile.mkdtemp(dir=self.slap.instance_directory)
    self.addCleanup(shutil.rmtree, self.working_directory)
    kvm_instance_partition = os.path.join(
      self.slap.instance_directory, self.kvm_instance_partition_reference)
    # find qemu_img from the tested SR via it's partition parameter, as
    # otherwise qemu-kvm would be dependency of test suite
    with open(
      os.path.join(self.computer_partition_root_path, 'buildout.cfg')) as fh:
      qemu_img = [
        q for q in fh.readlines()
        if 'raw qemu_img_executable_location' in q][0].split()[-1]

    self.first_disk = os.path.join(self.working_directory, 'first_disk')
    subprocess.check_call([
      qemu_img, "create", "-f", "qcow", self.first_disk, "1M"])
    second_disk = 'second_disk'
    self.second_disk = os.path.join(kvm_instance_partition, second_disk)
    subprocess.check_call([
      qemu_img, "create", "-f", "qcow2", os.path.join(
        kvm_instance_partition, self.second_disk), "1M"])
    self.third_disk = os.path.join(self.working_directory, 'third_disk')
    subprocess.check_call([
      qemu_img, "create", "-f", "qcow2", self.third_disk, "1M"])
    self.rerequestInstance({'_': json.dumps(
        self.getExternalDiskInstanceParameterDict(
          self.first_disk, second_disk, self.third_disk))})
    self.raising_waitForInstance(10)


@skipUnlessKvm
class TestInstanceHttpServer(InstanceTestCase, KvmMixin):
  __partition_reference__ = 'ihs'

  @classmethod
  def startHttpServer(cls):
    cls.http_directory = tempfile.mkdtemp()
    server = SocketServer.TCPServer(
      (cls._ipv4_address, findFreeTCPPort(cls._ipv4_address)),
      HttpHandler)

    bootstrap_script = b'bootstrap_script'
    cls.bootstrap_script_md5sum = hashlib.md5(bootstrap_script).hexdigest()
    with open(os.path.join(
      cls.http_directory, cls.bootstrap_script_md5sum), 'wb') as fh:
      fh.write(bootstrap_script)

    url = 'http://%s:%s' % server.server_address
    cls.bootstrap_script_url = '/'.join([url, cls.bootstrap_script_md5sum])

    old_dir = os.path.realpath(os.curdir)
    os.chdir(cls.http_directory)
    try:
      cls.server_process = multiprocessing.Process(
        target=server.serve_forever, name='HttpServer')
      cls.server_process.start()
    finally:
      os.chdir(old_dir)

  @classmethod
  def stopHttpServer(cls):
    cls.logger.debug(f'Stopping process {cls.server_process}')
    cls.server_process.join(10)
    cls.server_process.terminate()
    time.sleep(0.1)
    if cls.server_process.is_alive():
      cls.logger.warning(
        f'Process {cls.server_process} still alive')

    shutil.rmtree(cls.http_directory)

  @classmethod
  def setUpClass(cls):
    cls.startHttpServer()
    super().setUpClass()

  @classmethod
  def tearDownClass(cls):
    super().tearDownClass()
    cls.stopHttpServer()

  @classmethod
  def getInstanceParameterDict(cls):
    return {
      'enable-http-server': True,
      'bootstrap-script-url': '{}#{}'.format(
        cls.bootstrap_script_url, cls.bootstrap_script_md5sum),
      'data-to-vm': """data
to
vm""",
    }

  def test(self):
    connection_parameter_dict = self.getConnectionParameterDictJson()
    present_key_list = []
    assert_key_list = [
     'backend-url', 'url', 'monitor-setup-url', 'ipv6-network-info',
     'tap-ipv4', 'tap-ipv6']
    for k in assert_key_list:
      if k in connection_parameter_dict:
        present_key_list.append(k)
        connection_parameter_dict.pop(k)
    self.assertEqual(
      connection_parameter_dict,
      {
        'ipv6': self._ipv6_address,
        'maximum-extra-disk-amount': '0',
        'monitor-base-url': f'https://[{self._ipv6_address}]:8026',
        'nat-rule-port-tcp-22': f'{self._ipv6_address} : 10022',
        'nat-rule-port-tcp-443': f'{self._ipv6_address} : 10443',
        'nat-rule-port-tcp-80': f'{self._ipv6_address} : 10080',
      }
    )
    self.assertEqual(set(present_key_list), set(assert_key_list))
    self.assertEqual(
      """ihs0:6tunnel-10022-{hash}-on-watch RUNNING
ihs0:6tunnel-10080-{hash}-on-watch RUNNING
ihs0:6tunnel-10443-{hash}-on-watch RUNNING
ihs0:bootstrap-monitor EXITED
ihs0:certificate_authority-{hash}-on-watch RUNNING
ihs0:crond-{hash}-on-watch RUNNING
ihs0:http-server-{hash}-on-watch RUNNING
ihs0:kvm-{kvm-hash-value}-on-watch RUNNING
ihs0:kvm_controller EXITED
ihs0:monitor-httpd-{hash}-on-watch RUNNING
ihs0:monitor-httpd-graceful EXITED
ihs0:nginx-graceful EXITED
ihs0:nginx-on-watch RUNNING
ihs0:whitelist-domains-download-{hash} RUNNING
ihs0:whitelist-firewall-{hash} RUNNING""",
      self.getProcessInfo()
    )
    public_dir = os.path.join(
      self.computer_partition_root_path, 'srv', 'public')
    self.assertEqual(
      ['data', 'gateway', 'hostname', 'ipv4',
       'ipv6_config.sh', 'netmask', 'network', 'vm-bootstrap'],
      sorted(os.listdir(public_dir))
    )
    with open(os.path.join(public_dir, 'data')) as fh:
      self.assertEqual("""data
to
vm""", fh.read())
    with open(os.path.join(public_dir, 'vm-bootstrap')) as fh:
      self.assertEqual('bootstrap_script', fh.read())


@skipUnlessKvm
class TestInstanceHttpServerJson(
  KvmMixinJson, TestInstanceHttpServer):
  pass
