utils.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. # Copyright 2016 Cloudbase Solutions Srl
  2. # All Rights Reserved.
  3. import functools
  4. import hashlib
  5. import io
  6. import json
  7. import os
  8. import pickle
  9. import re
  10. import socket
  11. import subprocess
  12. import time
  13. import traceback
  14. import OpenSSL
  15. from oslo_config import cfg
  16. from oslo_log import log as logging
  17. from oslo_serialization import jsonutils
  18. import paramiko
  19. from coriolis import constants
  20. from coriolis import exception
  21. from coriolis import secrets
  22. opts = [
  23. cfg.StrOpt('qemu_img_path',
  24. default='qemu-img',
  25. help='The path of the qemu-img tool.'),
  26. ]
  27. CONF = cfg.CONF
  28. logging.register_options(CONF)
  29. CONF.register_opts(opts)
  30. LOG = logging.getLogger(__name__)
  31. def setup_logging():
  32. logging.setup(CONF, 'coriolis')
  33. def ignore_exceptions(func):
  34. @functools.wraps(func)
  35. def _ignore_exceptions(*args, **kwargs):
  36. try:
  37. return func(*args, **kwargs)
  38. except Exception as ex:
  39. LOG.exception(ex)
  40. return _ignore_exceptions
  41. def get_single_result(lis):
  42. """ Indexes the head of a single element list.
  43. Raises a KeyError if the list is empty or its length is greater than 1.
  44. """
  45. if len(lis) == 0:
  46. raise KeyError("Result list is empty.")
  47. elif len(lis) > 1:
  48. raise KeyError("More than one result in list: '%s'" % lis)
  49. return lis[0]
  50. def retry_on_error(max_attempts=5, sleep_seconds=0,
  51. terminal_exceptions=[]):
  52. def _retry_on_error(func):
  53. @functools.wraps(func)
  54. def _exec_retry(*args, **kwargs):
  55. i = 0
  56. while True:
  57. try:
  58. return func(*args, **kwargs)
  59. except KeyboardInterrupt as ex:
  60. LOG.debug("Got a KeyboardInterrupt, skip retrying")
  61. LOG.exception(ex)
  62. raise
  63. except Exception as ex:
  64. if any([isinstance(ex, tex)
  65. for tex in terminal_exceptions]):
  66. raise
  67. i += 1
  68. if i < max_attempts:
  69. LOG.warn("Exception occurred, retrying: %s", ex)
  70. time.sleep(sleep_seconds)
  71. else:
  72. raise
  73. return _exec_retry
  74. return _retry_on_error
  75. def get_udev_net_rules(net_ifaces_info):
  76. content = ""
  77. for name, mac_address in net_ifaces_info:
  78. content += ('SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", '
  79. 'ATTR{address}=="%(mac_address)s", NAME="%(name)s"\n' %
  80. {"name": name, "mac_address": mac_address.lower()})
  81. return content
  82. def get_linux_os_info(ssh):
  83. out = exec_ssh_cmd(ssh, "lsb_release -a || true").decode()
  84. dist_id = re.findall('^Distributor ID:\s(.*)$', out, re.MULTILINE)
  85. release = re.findall('^Release:\s(.*)$', out, re.MULTILINE)
  86. if dist_id and release:
  87. return (dist_id[0], release[0])
  88. @retry_on_error()
  89. def test_ssh_path(ssh, remote_path):
  90. sftp = ssh.open_sftp()
  91. try:
  92. sftp.stat(remote_path)
  93. return True
  94. except IOError as ex:
  95. if ex.args[0] == 2:
  96. return False
  97. raise
  98. @retry_on_error()
  99. def read_ssh_file(ssh, remote_path):
  100. sftp = ssh.open_sftp()
  101. return sftp.open(remote_path, 'rb').read()
  102. @retry_on_error()
  103. def write_ssh_file(ssh, remote_path, content):
  104. sftp = ssh.open_sftp()
  105. sftp.open(remote_path, 'wb').write(content)
  106. @retry_on_error()
  107. def list_ssh_dir(ssh, remote_path):
  108. sftp = ssh.open_sftp()
  109. return sftp.listdir(remote_path)
  110. @retry_on_error()
  111. def exec_ssh_cmd(ssh, cmd):
  112. LOG.debug("Executing SSH command: %s", cmd)
  113. stdin, stdout, stderr = ssh.exec_command(cmd)
  114. exit_code = stdout.channel.recv_exit_status()
  115. std_out = stdout.read()
  116. std_err = stderr.read()
  117. if exit_code:
  118. raise exception.CoriolisException(
  119. "Command \"%s\" failed with exit code: %s\n"
  120. "stdout: %s\nstd_err: %s" %
  121. (cmd, exit_code, std_out, std_err))
  122. return std_out
  123. def exec_ssh_cmd_chroot(ssh, chroot_dir, cmd):
  124. return exec_ssh_cmd(ssh, "sudo chroot %s %s" % (chroot_dir, cmd))
  125. def check_fs(ssh, fs_type, dev_path):
  126. try:
  127. out = exec_ssh_cmd(
  128. ssh, "sudo fsck -p -t %s %s" % (fs_type, dev_path)).decode()
  129. LOG.debug("File system checked:\n%s", out)
  130. except Exception as ex:
  131. LOG.warn("Checking file system returned an error:\n%s", str(ex))
  132. def _check_port_open(host, port):
  133. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  134. try:
  135. s.settimeout(1)
  136. s.connect((host, port))
  137. return True
  138. except (ConnectionRefusedError, socket.timeout, OSError):
  139. return False
  140. finally:
  141. s.close()
  142. def wait_for_port_connectivity(address, port, max_wait=300):
  143. i = 0
  144. while not _check_port_open(address, port) and i < max_wait:
  145. time.sleep(1)
  146. i += 1
  147. if i == max_wait:
  148. raise exception.CoriolisException("Connection failed on port %s" %
  149. port)
  150. def exec_process(args):
  151. p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  152. std_out, std_err = p.communicate()
  153. if p.returncode:
  154. raise exception.CoriolisException(
  155. "Command \"%s\" failed with exit code: %s\nstdout: %s\nstd_err: %s"
  156. % (args, p.returncode, std_out, std_err))
  157. return std_out
  158. def get_disk_info(disk_path):
  159. out = exec_process([CONF.qemu_img_path, 'info', '--output=json',
  160. disk_path])
  161. disk_info = json.loads(out.decode())
  162. if disk_info["format"] == "vpc":
  163. disk_info["format"] = constants.DISK_FORMAT_VHD
  164. return disk_info
  165. def convert_disk_format(disk_path, target_disk_path, target_format,
  166. preallocated=False):
  167. allocation_args = []
  168. if preallocated:
  169. if target_format != constants.DISK_FORMAT_VHD:
  170. raise NotImplementedError(
  171. "Preallocation is supported only for the VHD format.")
  172. allocation_args = ['-o', 'subformat=fixed']
  173. if target_format == constants.DISK_FORMAT_VHD:
  174. target_format = "vpc"
  175. args = ([CONF.qemu_img_path, 'convert', '-O', target_format] +
  176. allocation_args +
  177. [disk_path, target_disk_path])
  178. try:
  179. exec_process(args)
  180. except Exception:
  181. ignore_exceptions(os.remove)(target_disk_path)
  182. raise
  183. def get_hostname():
  184. return socket.gethostname()
  185. def get_exception_details():
  186. return traceback.format_exc()
  187. def walk_class_hierarchy(clazz, encountered=None):
  188. """Walk class hierarchy, yielding most derived classes first."""
  189. if not encountered:
  190. encountered = []
  191. for subclass in clazz.__subclasses__():
  192. if subclass not in encountered:
  193. encountered.append(subclass)
  194. # drill down to leaves first
  195. for subsubclass in walk_class_hierarchy(subclass, encountered):
  196. yield subsubclass
  197. yield subclass
  198. def get_ssl_cert_thumbprint(context, host, port=443, digest_algorithm="sha1"):
  199. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  200. ssl_sock = context.wrap_socket(sock, server_hostname=host)
  201. ssl_sock.connect((host, port))
  202. # binary_form is the only option when the certificate is not validated
  203. cert = ssl_sock.getpeercert(binary_form=True)
  204. sock.close()
  205. x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, cert)
  206. return x509.digest('sha1').decode()
  207. def _get_base_dir():
  208. return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  209. def get_resources_dir():
  210. return os.path.join(_get_base_dir(), "resources")
  211. def serialize_key(key, password=None):
  212. key_io = io.StringIO()
  213. key.write_private_key(key_io, password)
  214. return key_io.getvalue()
  215. def deserialize_key(key_bytes, password=None):
  216. key_io = io.StringIO(key_bytes)
  217. return paramiko.RSAKey.from_private_key(key_io, password)
  218. def is_serializable(obj):
  219. pickle.dumps(obj)
  220. def to_dict(obj, max_depth=10):
  221. # jsonutils.dumps() has a max_depth of 3 by default
  222. def _to_primitive(value, convert_instances=False,
  223. convert_datetime=True, level=0,
  224. max_depth=max_depth):
  225. return jsonutils.to_primitive(
  226. value, convert_instances, convert_datetime, level, max_depth)
  227. return jsonutils.loads(jsonutils.dumps(obj, default=_to_primitive))
  228. def topological_graph_sorting(items, id="id", depends_on="depends_on",
  229. sort_key=None):
  230. """
  231. Kahn's algorithm
  232. """
  233. if sort_key:
  234. # Sort siblings
  235. items = sorted(items, key=lambda t: t[sort_key], reverse=True)
  236. a = []
  237. for i in items:
  238. a.append({"id": i[id],
  239. "depends_on": list(i[depends_on] or []),
  240. "item": i})
  241. s = []
  242. l = []
  243. for n in a:
  244. if not n["depends_on"]:
  245. s.append(n)
  246. while s:
  247. n = s.pop()
  248. l.append(n["item"])
  249. for m in a:
  250. if n["id"] in m["depends_on"]:
  251. m["depends_on"].remove(n["id"])
  252. if not m["depends_on"]:
  253. s.append(m)
  254. if len(l) != len(a):
  255. raise ValueError("The graph contains cycles")
  256. return l
  257. def load_class(class_path):
  258. LOG.debug('Loading class \'%s\'' % class_path)
  259. parts = class_path.rsplit('.', 1)
  260. module = __import__(parts[0], fromlist=parts[1])
  261. return getattr(module, parts[1])
  262. def check_md5(data, md5):
  263. m = hashlib.md5()
  264. m.update(data)
  265. new_md5 = m.hexdigest()
  266. if new_md5 != md5:
  267. raise exception.CoriolisException("MD5 check failed")
  268. def get_secret_connection_info(ctxt, connection_info):
  269. secret_ref = connection_info.get("secret_ref")
  270. if secret_ref:
  271. LOG.info("Retrieving connection info from secret: %s", secret_ref)
  272. connection_info = secrets.get_secret(ctxt, secret_ref)
  273. return connection_info
  274. def parse_int_value(value):
  275. try:
  276. return int(str(value))
  277. except ValueError:
  278. raise exception.InvalidInput("Invalid integer: %s" % value)