Jelajahi Sumber

Adds Windows support

Alessandro Pilotti 10 tahun lalu
induk
melakukan
f260ad8102

+ 3 - 0
coriolis/constants.py

@@ -31,3 +31,6 @@ PLATFORM_VMWARE_VSPHERE = "vmware_vsphere"
 TASK_EVENT_INFO = "INFO"
 TASK_EVENT_WARNING = "WARNING"
 TASK_EVENT_ERROR = "ERROR"
+
+OS_TYPE_LINUX = "linux"
+OS_TYPE_WINDOWS = "windows"

+ 4 - 0
coriolis/exception.py

@@ -233,3 +233,7 @@ class NotSupportedOperation(Invalid):
 
 class TaskProcessException(CoriolisException):
     pass
+
+
+class OperatingSystemNotFound(NotFound):
+    pass

+ 59 - 47
coriolis/osmorphing/base.py

@@ -9,10 +9,8 @@ from coriolis import utils
 class BaseOSMorphingTools(object):
     __metaclass__ = abc.ABCMeta
 
-    _packages = {}
-
-    def __init__(self, ssh, os_root_dir, hypervisor, platform, event_manager):
-        self._ssh = ssh
+    def __init__(self, conn, os_root_dir, hypervisor, platform, event_manager):
+        self._conn = conn
         self._os_root_dir = os_root_dir
         self._hypervisor = hypervisor
         self._platform = platform
@@ -20,6 +18,63 @@ class BaseOSMorphingTools(object):
         self._version = None
         self._event_manager = event_manager
 
+    def check_os(self):
+        if not self._distro:
+            os_info = self._check_os()
+            if os_info:
+                self._distro, self._version = os_info
+        if self._distro:
+            return self._distro, self._version
+
+    @abc.abstractmethod
+    def _check_os(self):
+        pass
+
+    @abc.abstractmethod
+    def set_net_config(self, nics_info, dhcp):
+        pass
+
+    def get_packages(self):
+        return [], []
+
+    def pre_packages_install(self):
+        pass
+
+    def install_packages(self, package_names):
+        pass
+
+    def uninstall_packages(self, package_names):
+        pass
+
+    def post_packages_install(self):
+        pass
+
+
+class BaseLinuxOSMorphingTools(BaseOSMorphingTools):
+    __metaclass__ = abc.ABCMeta
+
+    _packages = {}
+
+    def __init__(self, conn, os_root_dir, hypervisor, platform, event_manager):
+        super(BaseLinuxOSMorphingTools, self).__init__(
+            conn, os_root_dir, hypervisor, platform, event_manager)
+        self._ssh = conn
+
+    def get_packages(self):
+        k_add = [(h, p) for (h, p) in self._packages.keys() if
+                 (h is None or h == self._hypervisor) and
+                 (p is None or p == self._platform)]
+
+        add = [p[0] for p in itertools.chain.from_iterable(
+               [l for k, l in self._packages.items() if k in k_add])]
+
+        k_remove = set(self._packages.keys()) - set(k_add)
+        remove = [p[0] for p in itertools.chain.from_iterable(
+                  [l for k, l in self._packages.items() if k in k_remove])
+                  if p[1]]
+
+        return add, remove
+
     def _test_path(self, chroot_path):
         path = os.path.join(self._os_root_dir, chroot_path)
         return utils.test_ssh_path(self._ssh, path)
@@ -55,46 +110,3 @@ class BaseOSMorphingTools(object):
         self._write_file(tmp_file, content)
         self._exec_cmd_chroot("cp /%s /%s" % (tmp_file, chroot_path))
         self._exec_cmd_chroot("rm /%s" % tmp_file)
-
-    def check_os(self):
-        if not self._distro:
-            os_info = self._check_os()
-            if os_info:
-                self._distro, self._version = os_info
-        if self._distro:
-            return self._distro, self._version
-
-    @abc.abstractmethod
-    def _check_os(self):
-        pass
-
-    def get_packages(self):
-        k_add = [(h, p) for (h, p) in self._packages.keys() if
-                 (h is None or h == self._hypervisor) and
-                 (p is None or p == self._platform)]
-
-        add = [p[0] for p in itertools.chain.from_iterable(
-               [l for k, l in self._packages.items() if k in k_add])]
-
-        k_remove = set(self._packages.keys()) - set(k_add)
-        remove = [p[0] for p in itertools.chain.from_iterable(
-                  [l for k, l in self._packages.items() if k in k_remove])
-                  if p[1]]
-
-        return add, remove
-
-    @abc.abstractmethod
-    def set_net_config(self, nics_info, dhcp):
-        pass
-
-    def pre_packages_install(self):
-        pass
-
-    def install_packages(self, package_names):
-        pass
-
-    def uninstall_packages(self, package_names):
-        pass
-
-    def post_packages_install(self):
-        pass

+ 1 - 1
coriolis/osmorphing/debian.py

@@ -5,7 +5,7 @@ from coriolis import constants
 from coriolis.osmorphing import base
 
 
-class DebianMorphingTools(base.BaseOSMorphingTools):
+class DebianMorphingTools(base.BaseLinuxOSMorphingTools):
     _packages = {
         (constants.HYPERVISOR_VMWARE, None): [("open-vm-tools", True)],
         # TODO: add cloud-initramfs-growroot

+ 17 - 6
coriolis/osmorphing/factory.py

@@ -1,17 +1,28 @@
+import itertools
+
+from coriolis import constants
 from coriolis import exception
 from coriolis.osmorphing import debian
 from coriolis.osmorphing import redhat
 from coriolis.osmorphing import ubuntu
+from coriolis.osmorphing import windows
 
 
-def get_os_morphing_tools(ssh, os_root_dir, target_hypervisor,
+def get_os_morphing_tools(conn, os_type, os_root_dir, target_hypervisor,
                           target_platform, event_manager):
-    os_morphing_tools_clss = [debian.DebianMorphingTools,
-                              ubuntu.UbuntuMorphingTools,
-                              redhat.RedHatMorphingTools]
+    os_morphing_tools_clss = {
+        constants.OS_TYPE_LINUX: [debian.DebianMorphingTools,
+                                  ubuntu.UbuntuMorphingTools,
+                                  redhat.RedHatMorphingTools],
+        constants.OS_TYPE_WINDOWS: [windows.WindowsMorphingTools],
+        }
+
+    if os_type and os_type not in os_morphing_tools_clss:
+        raise exception.CoriolisException("Unsupported OS type: %s" % os_type)
 
-    for cls in os_morphing_tools_clss:
-        tools = cls(ssh, os_root_dir, target_hypervisor, target_platform,
+    for cls in os_morphing_tools_clss.get(
+            os_type, itertools.chain(*os_morphing_tools_clss.values())):
+        tools = cls(conn, os_root_dir, target_hypervisor, target_platform,
                     event_manager)
         os_info = tools.check_os()
         if os_info:

+ 7 - 16
coriolis/osmorphing/manager.py

@@ -1,5 +1,4 @@
 from oslo_log import log as logging
-import paramiko
 
 from coriolis.osmorphing import factory as osmorphing_factory
 from coriolis.osmorphing.osmount import factory as osmount_factory
@@ -8,26 +7,18 @@ from coriolis import utils
 LOG = logging.getLogger(__name__)
 
 
-def morph_image(connection_info, target_hypervisor, target_platform,
+def morph_image(connection_info, os_type, target_hypervisor, target_platform,
                 volume_devs, nics_info, event_manager):
-    (ip, port, username, pkey) = connection_info
-
-    LOG.info("Waiting for connectivity on host: %(ip)s:%(port)s",
-             {"ip": ip, "port": port})
-    utils.wait_for_port_connectivity(ip, port)
-
-    event_manager.progress_update(
-        "Connecting to host: %(ip)s:%(port)s" % {"ip": ip, "port": port})
-    ssh = paramiko.SSHClient()
-    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-    ssh.connect(hostname=ip, port=port, username=username, pkey=pkey)
-
-    os_mount_tools = osmount_factory.get_os_mount_tools(ssh, event_manager)
+    os_mount_tools = osmount_factory.get_os_mount_tools(
+        os_type, connection_info, event_manager)
 
     event_manager.progress_update("Discovering and mounting OS partitions")
     os_root_dir, other_mounted_dirs = os_mount_tools.mount_os(volume_devs)
+
+    conn = os_mount_tools.get_connection()
     os_morphing_tools, os_info = osmorphing_factory.get_os_morphing_tools(
-        ssh, os_root_dir, target_hypervisor, target_platform, event_manager)
+        conn, os_type, os_root_dir, target_hypervisor, target_platform,
+        event_manager)
 
     event_manager.progress_update('OS being migrated: %s' % str(os_info))
 

+ 37 - 4
coriolis/osmorphing/osmount/base.py

@@ -1,14 +1,24 @@
 import abc
 
+import paramiko
+
 from coriolis import utils
 
 
 class BaseOSMountTools(object):
     __metaclass__ = abc.ABCMeta
 
-    def __init__(self, ssh, event_manager):
-        self._ssh = ssh
+    def __init__(self, connection_info, event_manager):
         self._event_manager = event_manager
+        self._connect(connection_info)
+
+    @abc.abstractmethod
+    def _connect(self, connection_info):
+        pass
+
+    @abc.abstractmethod
+    def get_connection(self):
+        pass
 
     @abc.abstractmethod
     def check_os(self):
@@ -22,5 +32,28 @@ class BaseOSMountTools(object):
     def dismount_os(self, dirs):
         pass
 
-    def _exec_cmd(self, cmd):
-        return utils.exec_ssh_cmd(self._ssh, cmd)
+
+class BaseSSHOSMountTools(object):
+
+    def _connect(self, connection_info):
+        ip = connection_info["ip"]
+        port = connection_info.get("port", 22)
+        username = connection_info["username"]
+        pkey = connection_info.get("pkey")
+        password = connection_info.get("password")
+
+        LOG.info("Waiting for connectivity on host: %(ip)s:%(port)s",
+                 {"ip": ip, "port": port})
+        utils.wait_for_port_connectivity(ip, port)
+
+        self._event_manager.progress_update(
+            "Connecting to SSH host: %(ip)s:%(port)s" %
+            {"ip": ip, "port": port})
+        ssh = paramiko.SSHClient()
+        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+        ssh.connect(hostname=ip, port=port, username=username, pkey=pkey,
+                    password=password)
+        self._ssh = ssh
+
+    def get_connection(self):
+        return self._ssh

+ 13 - 4
coriolis/osmorphing/osmount/factory.py

@@ -1,12 +1,21 @@
+import itertools
+
+from coriolis import constants
 from coriolis import exception
 from coriolis.osmorphing.osmount import ubuntu
+from coriolis.osmorphing.osmount import windows
+
 
+def get_os_mount_tools(os_type, connection_info, event_manager):
+    os_mount_tools = {constants.OS_TYPE_LINUX: [ubuntu.UbuntuOSMountTools],
+                      constants.OS_TYPE_WINDOWS: [windows.WindowsMountTools]}
 
-def get_os_mount_tools(ssh, event_manager):
-    os_mount_tools = [ubuntu.UbuntuOSMountTools]
+    if os_type and os_type not in os_mount_tools:
+        raise exception.CoriolisException("Unsupported OS type: %s" % os_type)
 
-    for cls in os_mount_tools:
-        tools = cls(ssh, event_manager)
+    for cls in os_mount_tools.get(os_type,
+                                  itertools.chain(*os_mount_tools.values())):
+        tools = cls(connection_info, event_manager)
         if tools.check_os():
             return tools
     raise exception.CoriolisException("OS mount tools not found")

+ 5 - 2
coriolis/osmorphing/osmount/ubuntu.py

@@ -6,12 +6,15 @@ from coriolis.osmorphing.osmount import base
 from coriolis import utils
 
 
-class UbuntuOSMountTools(base.BaseOSMountTools):
+class UbuntuOSMountTools(base.BaseSSHOSMountTools):
     def check_os(self):
         os_info = utils.get_linux_os_info(self._ssh)
         if os_info and os_info[0] == 'Ubuntu':
             return True
 
+    def _exec_cmd(self, cmd):
+        return utils.exec_ssh_cmd(self._ssh, cmd)
+
     def _get_vgnames(self):
         vg_names = []
         vgscan_out_lines = self._exec_cmd(
@@ -78,7 +81,7 @@ class UbuntuOSMountTools(base.BaseOSMountTools):
                 break
 
         if not os_root_dir:
-            raise exception.CoriolisException("root partition not found")
+            raise exception.OperatingSystemNotFound("root partition not found")
 
         for dir in set(dirs).intersection(['proc', 'sys', 'dev', 'run']):
             mount_dir = os.path.join(os_root_dir, dir)

+ 84 - 0
coriolis/osmorphing/osmount/windows.py

@@ -0,0 +1,84 @@
+import os
+import re
+
+from oslo_log import log as logging
+
+from coriolis import exception
+from coriolis.osmorphing.osmount import base
+from coriolis import utils
+from coriolis import wsman
+
+LOG = logging.getLogger(__name__)
+
+
+class WindowsMountTools(base.BaseOSMountTools):
+    def _connect(self, connection_info):
+        host = connection_info["ip"]
+        port = connection_info.get("port", 5986)
+        username = connection_info["username"]
+        password = connection_info.get("password")
+        cert_pem = connection_info.get("cert_pem")
+        cert_key_pem = connection_info.get("cert_key_pem")
+        url = "https://%s:%s/wsman" % (host, port)
+
+        LOG.info("Waiting for connectivity on host: %(host)s:%(port)s",
+                 {"host": host, "port": port})
+        utils.wait_for_port_connectivity(host, port)
+
+        self._event_manager.progress_update(
+            "Connecting to WinRM host: %(host)s:%(port)s" %
+            {"host": host, "port": port})
+
+        conn = wsman.WSManConnection()
+        conn.connect(url=url, username=username, password=password,
+                     cert_pem=cert_pem, cert_key_pem=cert_key_pem)
+        self._conn = conn
+
+    def get_connection(self):
+        return self._conn
+
+    def check_os(self):
+        try:
+            version_info = self._conn.exec_ps_command(
+                "(get-ciminstance Win32_OperatingSystem).Caption")
+            LOG.debug("Windows version: %s", version_info)
+            return True
+        except exception.CoriolisException:
+            pass
+
+    def _refresh_storage(self):
+        self._conn.exec_ps_command(
+            "Update-HostStorageCache", ignore_stdout=True)
+
+    def _bring_all_disks_online(self):
+        self._conn.exec_ps_command(
+            "Get-Disk |? IsOffline | Set-Disk -IsOffline $False",
+            ignore_stdout=True)
+
+    def _bring_disk_offline(self, drive_letter):
+        self._conn.exec_ps_command(
+            "Get-Volume |? DriveLetter -eq \"%s\" | Get-Partition | "
+            "Get-Disk | Set-Disk -IsOffline $True" % drive_letter,
+            ignore_stdout=True)
+
+    def _get_system_drive(self):
+        return self._conn.exec_ps_command("$env:SystemDrive")
+
+    def _get_fs_roots(self):
+        return self._conn.exec_ps_command(
+            "(get-psdrive -PSProvider FileSystem).Root").split(self._conn.EOL)
+
+    def mount_os(self, volume_devs):
+        self._refresh_storage()
+        self._bring_all_disks_online()
+        fs_roots = self._get_fs_roots()
+        system_drive = self._get_system_drive()
+
+        for fs_root in [r for r in fs_roots if not r[:-1] == system_drive]:
+            if self._conn.test_path("%sWindows\\System32" % fs_root):
+                return fs_root, []
+
+    def dismount_os(self, dirs):
+        for dir in dirs:
+            drive_letter = dir.split(":")[0]
+            self._bring_disk_offline(drive_letter)

+ 1 - 1
coriolis/osmorphing/redhat.py

@@ -19,7 +19,7 @@ RELEASE_FEDORA = "Fedora"
 DEFAULT_CLOUD_USER = "cloud-user"
 
 
-class RedHatMorphingTools(base.BaseOSMorphingTools):
+class RedHatMorphingTools(base.BaseLinuxOSMorphingTools):
     _packages = {
         (None, None): [("dracut-config-generic", False)],
         (constants.HYPERVISOR_VMWARE, None): [("open-vm-tools", True)],

+ 123 - 0
coriolis/osmorphing/windows.py

@@ -0,0 +1,123 @@
+import os
+import re
+
+from distutils import version
+from oslo_config import cfg
+from oslo_log import log as logging
+
+from coriolis import constants
+from coriolis import exception
+from coriolis.osmorphing import base
+
+opts = [
+    cfg.StrOpt('virtio_iso_url',
+               default='https://fedorapeople.org/groups/virt/virtio-win/'
+               'direct-downloads/latest-virtio/virtio-win.iso',
+               help="Location of the virtio-win ISO"),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(opts, 'windows_images')
+
+LOG = logging.getLogger(__name__)
+
+
+class WindowsMorphingTools(base.BaseOSMorphingTools):
+    def _check_os(self):
+        try:
+            version = self._get_image_version()
+            return ('Windows', version)
+        except exception.CoriolisException:
+            pass
+
+    def pre_packages_install(self):
+        if self._hypervisor == constants.HYPERVISOR_KVM:
+            self._add_virtio_drivers()
+
+        if self._platform == constants.PLATFORM_OPENSTACK:
+            self._add_cloudbase_init()
+
+    def set_net_config(self, nics_info, dhcp):
+        # TODO: implement
+        pass
+
+    def _get_dism_features(self):
+        LOG.info("Getting image features")
+        return self._conn.exec_command(
+            "dism.exe", ["/get-features",  "/image:%s" % self._os_root_dir])
+
+    def _add_dism_driver(self, driver_path):
+        LOG.info("Adding driver: %s" % driver_path)
+        return self._conn.exec_command(
+            "dism.exe /add-driver /image:%s /driver:%s /recurse /forceunsigned"
+            % (self._os_root_dir, driver_path))
+
+    def _get_image_version(self):
+        features = self._get_dism_features()
+        m = re.search(r'^Image Version: (.*)$',
+                      features.replace(self._conn.EOL, os.linesep),
+                      re.MULTILINE)
+        if not m:
+            raise exception.CoriolisException("Could not find OS version")
+        return m.groups()[0]
+
+    def _mount_disk_image(self, path):
+        LOG.info("Mounting disk image: %s" % path)
+        return self._conn.exec_ps_command(
+            "(Mount-DiskImage '%s' -PassThru | Get-Volume).DriveLetter" %
+            path)
+
+    def _dismount_disk_image(self, path):
+        LOG.info("Unmounting disk image: %s" % path)
+        self._conn.exec_ps_command("Dismount-DiskImage '%s'" % path,
+                                   ignore_stdout=True)
+
+    def _add_virtio_drivers(self):
+        self._event_manager.progress_update("Adding virtio-win drivers")
+
+        # TODO: add support for x86
+        arch = "amd64"
+
+        # Ordered by version number
+        # TODO(alexpilotti): distinguish client and server
+        virtio_dirs = [
+            ("xp", version.LooseVersion("5.1")),
+            ("2k3", version.LooseVersion("5.2")),
+            ("2k8", version.LooseVersion("6.0")),
+            ("w7", version.LooseVersion("6.1")),
+            ("2k8R2", version.LooseVersion("6.1")),
+            ("w8", version.LooseVersion("6.2")),
+            ("2k12", version.LooseVersion("6.2")),
+            ("w8.1", version.LooseVersion("6.3")),
+            ("2k12R2", version.LooseVersion("6.3")),
+            ("w10", version.LooseVersion("10.0")),
+            ]
+
+        drivers = ["Balloon", "NetKVM", "qxldod", "pvpanic", "viorng",
+                   "vioscsi", "vioserial", "viostor"]
+
+        virtio_iso_path = "c:\\virtio-win.iso"
+        self._conn.download_file(
+            CONF.windows_images.virtio_iso_url, virtio_iso_path)
+
+        virtio_drive = self._mount_disk_image(virtio_iso_path)
+        try:
+            image_version = version.LooseVersion(self._version)
+
+            for virtio_dir, dir_version in reversed(virtio_dirs):
+                if image_version >= dir_version:
+                    path = "%s:\\Balloon\\%s\\%s" % (
+                        virtio_drive, virtio_dir, arch)
+                    if self._conn.test_path(path):
+                        break
+
+            driver_paths = ["%s:\\%s\\%s\\%s" % (
+                            virtio_drive, d, virtio_dir, arch)
+                            for d in drivers]
+            for driver_path in driver_paths:
+                self._add_dism_driver(driver_path)
+        finally:
+            self._dismount_disk_image(virtio_iso_path)
+
+    def _add_cloudbase_init(self):
+        self._event_manager.progress_update("Adding cloudbase-init")

+ 10 - 1
coriolis/providers/openstack/__init__.py

@@ -94,7 +94,12 @@ class _MigrationResources(object):
         self._k = k
 
     def get_guest_connection_info(self):
-        return (self._floating_ip.ip, SSH_PORT, MIGR_GUEST_USERNAME, self._k)
+        return {
+            "ip": self._floating_ip.ip,
+            "port": SSH_PORT,
+            "username": MIGR_GUEST_USERNAME,
+            "pkey": self._k,
+            }
 
     @utils.retry_on_error()
     def _wait_for_instance_deletion(self, instance_id):
@@ -407,9 +412,13 @@ class ImportProvider(base.BaseImportProvider):
 
                 guest_conn_info = migr_resources.get_guest_connection_info()
 
+
+            os_type = None
+
             self._event_manager.progress_update(
                 "Preparing instance for target platform")
             osmorphing_manager.morph_image(guest_conn_info,
+                                           os_type,
                                            hypervisor_type,
                                            constants.PLATFORM_OPENSTACK,
                                            volume_devs,

+ 105 - 0
coriolis/wsman.py

@@ -0,0 +1,105 @@
+from oslo_log import log as logging
+from winrm import protocol
+
+from coriolis import exception
+from coriolis import utils
+
+AUTH_BASIC = "basic"
+AUTH_KERBEROS = "kerberos"
+AUTH_CERTIFICATE = "certificate"
+
+CODEPAGE_UTF8 = 65001
+
+LOG = logging.getLogger(__name__)
+
+
+class WSManConnection(object):
+    def __init__(self):
+        self._protocol = None
+        self._shell_id = None
+
+    EOL = "\r\n"
+
+    @utils.retry_on_error()
+    def connect(self, url, username, auth=None, password=None,
+                cert_pem=None, cert_key_pem=None):
+        protocol.Protocol.DEFAULT_TIMEOUT = 3600
+
+        if not auth:
+            if cert_pem:
+                auth = AUTH_CERTIFICATE
+            else:
+                auth = AUTH_BASIC
+
+        auth_transport_map = {AUTH_BASIC: 'plaintext',
+                              AUTH_KERBEROS: 'kerberos',
+                              AUTH_CERTIFICATE: 'ssl'}
+
+        self._protocol = protocol.Protocol(
+            endpoint=url,
+            transport=auth_transport_map[auth],
+            username=username,
+            password=password,
+            cert_pem=cert_pem,
+            cert_key_pem=cert_key_pem)
+
+        self._shell_id = self._protocol.open_shell(codepage=CODEPAGE_UTF8)
+
+    def disconnect(self):
+        self._protocol.close_shell(self._shell_id)
+        self._shell_id = None
+        self._protocol = None
+
+    @utils.retry_on_error()
+    def _exec_command(self, cmd, args=[]):
+        command_id = self._protocol.run_command(self._shell_id, cmd, args)
+        try:
+            std_out, std_err, exit_code = self._protocol.get_command_output(
+                self._shell_id, command_id)
+        finally:
+            self._protocol.cleanup_command(self._shell_id, command_id)
+
+        return (std_out, std_err, exit_code)
+
+    def exec_command(self, cmd, args=[]):
+        LOG.debug("Executing WSMAN command: %s", str([cmd] + args))
+        std_out, std_err, exit_code = self._exec_command(cmd, args)
+
+        if exit_code:
+            raise exception.CoriolisException(
+                "Command \"%s\" failed with exit code: %s\n"
+                "stdout: %s\nstd_err: %s" %
+                (str([cmd] + args), exit_code, std_out, std_err))
+
+        return std_out
+
+    def exec_ps_command(self, cmd, ignore_stdout=False):
+        # This is needed to avoid Nano Server's output formatting
+        if not ignore_stdout:
+            cmd_fmt = "\"%s | out-file out.txt\""
+        else:
+            cmd_fmt = "\"%s\""
+
+        std_out = self.exec_command("powershell.exe", [cmd_fmt % cmd])
+
+        if not ignore_stdout:
+            return self.exec_command("cmd.exe", ["/c", "type", "out.txt"])[:-2]
+
+    def test_path(self, remote_path):
+        ret_val = self.exec_ps_command("Test-Path -Path \"%s\"" % remote_path)
+        return ret_val == "True"
+
+    def download_file(self, url, remote_path):
+        LOG.debug("Downloading: \"%(url)s\" to \"%(path)s\"",
+                  {"url": url, "path": remote_path})
+        # Nano Server does not have Invoke-WebRequest and additionally
+        # this is also faster
+        self.exec_ps_command(
+            "if(!([System.Management.Automation.PSTypeName]'"
+            "System.Net.Http.HttpClient').Type) {$assembly = "
+            "[System.Reflection.Assembly]::LoadWithPartialName("
+            "'System.Net.Http')}; (new-object System.Net.Http.HttpClient)."
+            "GetStreamAsync('%(url)s').Result.CopyTo("
+            "(New-Object IO.FileStream '%(outfile)s', Create, Write, None), "
+            "1MB)" % {"url": url, "outfile": remote_path},
+            ignore_stdout=True)