wsman.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. # Copyright 2016 Cloudbase Solutions Srl
  2. # All Rights Reserved.
  3. import base64
  4. from oslo_log import log as logging
  5. import requests
  6. from winrm import protocol
  7. from winrm import exceptions as winrm_exceptions
  8. from coriolis import exception
  9. from coriolis import utils
  10. AUTH_BASIC = "basic"
  11. AUTH_KERBEROS = "kerberos"
  12. AUTH_CERTIFICATE = "certificate"
  13. CODEPAGE_UTF8 = 65001
  14. DEFAULT_TIMEOUT = 3600
  15. LOG = logging.getLogger(__name__)
  16. class WSManConnection(object):
  17. def __init__(self, timeout=None):
  18. self._protocol = None
  19. self._conn_timeout = int(timeout or DEFAULT_TIMEOUT)
  20. EOL = "\r\n"
  21. @utils.retry_on_error()
  22. def connect(self, url, username, auth=None, password=None,
  23. cert_pem=None, cert_key_pem=None):
  24. if not auth:
  25. if cert_pem:
  26. auth = AUTH_CERTIFICATE
  27. else:
  28. auth = AUTH_BASIC
  29. auth_transport_map = {AUTH_BASIC: 'plaintext',
  30. AUTH_KERBEROS: 'kerberos',
  31. AUTH_CERTIFICATE: 'ssl'}
  32. self._protocol = protocol.Protocol(
  33. endpoint=url,
  34. transport=auth_transport_map[auth],
  35. username=username,
  36. password=password,
  37. cert_pem=cert_pem,
  38. cert_key_pem=cert_key_pem)
  39. @classmethod
  40. def from_connection_info(cls, connection_info, timeout=DEFAULT_TIMEOUT):
  41. """ Returns a wsman.WSManConnection object for the provided conn info. """
  42. if not isinstance(connection_info, dict):
  43. raise ValueError(
  44. "WSMan connection must be a dict. Got type '%s', value: %s" % (
  45. type(connection_info), connection_info))
  46. required_keys = ["ip", "username", "password"]
  47. missing = [key for key in required_keys if key not in connection_info]
  48. if missing:
  49. raise ValueError(
  50. "The following keys were missing from WSMan connection info %s. "
  51. "Got: %s" % (missing, connection_info))
  52. host = connection_info["ip"]
  53. port = connection_info.get("port", 5986)
  54. username = connection_info["username"]
  55. password = connection_info.get("password")
  56. cert_pem = connection_info.get("cert_pem")
  57. cert_key_pem = connection_info.get("cert_key_pem")
  58. url = "https://%s:%s/wsman" % (host, port)
  59. LOG.info("Connection info: %s", str(connection_info))
  60. LOG.info("Waiting for connectivity on host: %(host)s:%(port)s",
  61. {"host": host, "port": port})
  62. utils.wait_for_port_connectivity(host, port)
  63. conn = cls(timeout)
  64. conn.connect(url=url, username=username, password=password,
  65. cert_pem=cert_pem, cert_key_pem=cert_key_pem)
  66. return conn
  67. def disconnect(self):
  68. self._protocol = None
  69. def set_timeout(self, timeout):
  70. if timeout:
  71. self._protocol.timeout = timeout
  72. self._protocol.transport.timeout = timeout
  73. @utils.retry_on_error(
  74. terminal_exceptions=[winrm_exceptions.InvalidCredentialsError,
  75. exception.OSMorphingWinRMOperationTimeout])
  76. def _exec_command(self, cmd, args=[], timeout=None):
  77. timeout = int(timeout or self._conn_timeout)
  78. self.set_timeout(timeout)
  79. shell_id = self._protocol.open_shell(codepage=CODEPAGE_UTF8)
  80. try:
  81. command_id = self._protocol.run_command(shell_id, cmd, args)
  82. try:
  83. (std_out,
  84. std_err,
  85. exit_code) = self._protocol.get_command_output(
  86. shell_id, command_id)
  87. except requests.exceptions.ReadTimeout:
  88. raise exception.OSMorphingWinRMOperationTimeout(
  89. cmd=("%s %s" % (cmd, " ".join(args))), timeout=timeout)
  90. finally:
  91. self._protocol.cleanup_command(shell_id, command_id)
  92. return (std_out, std_err, exit_code)
  93. finally:
  94. self._protocol.close_shell(shell_id)
  95. def exec_command(self, cmd, args=[], timeout=None):
  96. LOG.debug("Executing WSMAN command: %s", str([cmd] + args))
  97. std_out, std_err, exit_code = self._exec_command(
  98. cmd, args, timeout=timeout)
  99. if exit_code:
  100. raise exception.CoriolisException(
  101. "Command \"%s\" failed with exit code: %s\n"
  102. "stdout: %s\nstd_err: %s" %
  103. (str([cmd] + args), exit_code, std_out, std_err))
  104. return std_out
  105. def exec_ps_command(self, cmd, ignore_stdout=False, timeout=None):
  106. LOG.debug("Executing PS command: %s", cmd)
  107. base64_cmd = base64.b64encode(cmd.encode('utf-16le')).decode()
  108. return self.exec_command(
  109. "powershell.exe", ["-EncodedCommand", base64_cmd],
  110. timeout=timeout)[:-2]
  111. def test_path(self, remote_path):
  112. ret_val = self.exec_ps_command("Test-Path -Path \"%s\"" % remote_path)
  113. return ret_val == "True"
  114. def download_file(self, url, remote_path):
  115. LOG.debug("Downloading: \"%(url)s\" to \"%(path)s\"",
  116. {"url": url, "path": remote_path})
  117. try:
  118. # Nano Server does not have Invoke-WebRequest and additionally
  119. # this is also faster
  120. self.exec_ps_command(
  121. "[Net.ServicePointManager]::SecurityProtocol = "
  122. "[Net.SecurityProtocolType]::Tls12;"
  123. "if(!([System.Management.Automation.PSTypeName]'"
  124. "System.Net.Http.HttpClient').Type) {$assembly = "
  125. "[System.Reflection.Assembly]::LoadWithPartialName("
  126. "'System.Net.Http')}; (new-object System.Net.Http.HttpClient)."
  127. "GetStreamAsync('%(url)s').Result.CopyTo("
  128. "(New-Object IO.FileStream '%(outfile)s', Create, Write, "
  129. "None), 1MB)" % {"url": url, "outfile": remote_path},
  130. ignore_stdout=True)
  131. except exception.CoriolisException as ex:
  132. LOG.trace(utils.get_exception_details())
  133. raise exception.CoriolisException(
  134. "Failed to download file from URL: %s to path: %s. Please "
  135. "check logs for more details." % (
  136. url, remote_path)) from ex
  137. def write_file(self, remote_path, content):
  138. self.exec_ps_command(
  139. "[IO.File]::WriteAllBytes('%s', [Convert]::FromBase64String('%s'))"
  140. % (remote_path, base64.b64encode(content).decode()),
  141. ignore_stdout=True)