utils.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014
  1. # Copyright 2016 Cloudbase Solutions Srl
  2. # All Rights Reserved.
  3. # pylint: disable=anomalous-backslash-in-string
  4. import base64
  5. import binascii
  6. import copy
  7. import functools
  8. import hashlib
  9. import io
  10. import json
  11. import OpenSSL
  12. import os
  13. import pickle
  14. import re
  15. import socket
  16. import string
  17. import subprocess
  18. import sys
  19. import time
  20. import traceback
  21. import uuid
  22. from io import StringIO
  23. from oslo_config import cfg
  24. from oslo_log import log as logging
  25. from oslo_serialization import jsonutils
  26. import netifaces
  27. import paramiko
  28. # NOTE(gsamfira): I am aware that this is not ideal, but pip
  29. # developers have decided to move all logic inside an _internal
  30. # package, and I really don't want to do an exec call to pip
  31. # just to get installed packages and their versions, when I can
  32. # simply call a function.
  33. from pip._internal.operations import freeze
  34. from six.moves.urllib import parse
  35. from webob import exc
  36. from coriolis import constants
  37. from coriolis import exception
  38. from coriolis import secrets
  39. opts = [
  40. cfg.StrOpt('qemu_img_path',
  41. default='qemu-img',
  42. help='The path of the qemu-img tool.'),
  43. ]
  44. CONF = cfg.CONF
  45. logging.register_options(CONF)
  46. CONF.register_opts(opts)
  47. LOG = logging.getLogger(__name__)
  48. UNSPACED_MAC_ADDRESS_REGEX = "^([0-9a-f]{12})$"
  49. SPACED_MAC_ADDRESS_REGEX = "^(([0-9a-f]{2}:){5}([0-9a-f]{2}))$"
  50. SYSTEMD_TEMPLATE = """
  51. [Unit]
  52. Description=Coriolis %(svc_name)s
  53. After=network-online.target
  54. [Service]
  55. Type=simple
  56. ExecStart=%(cmdline)s
  57. Restart=always
  58. RestartSec=5s
  59. User=%(username)s
  60. [Install]
  61. WantedBy=multi-user.target
  62. """
  63. UPSTART_TEMPLATE = """
  64. # %(svc_name)s - Coriolis %(svc_name)s service
  65. #
  66. description "%(svc_name)s service"
  67. start on runlevel [2345]
  68. stop on runlevel [!2345]
  69. respawn
  70. umask 022
  71. exec %(cmdline)s
  72. """
  73. def _get_local_ips():
  74. ifaces = netifaces.interfaces()
  75. ret = []
  76. for iface in ifaces:
  77. if iface == "lo":
  78. continue
  79. addrs = netifaces.ifaddresses(iface)
  80. ret.append(
  81. {
  82. iface: {
  83. "ipv4": addrs.get(netifaces.AF_INET),
  84. "ipv6": addrs.get(netifaces.AF_INET6),
  85. },
  86. }
  87. )
  88. return ret
  89. def _get_host_os_info():
  90. info = None
  91. # This exists on all modern Linux systems
  92. if os.path.isfile("/etc/os-release"):
  93. with open("/etc/os-release") as fd:
  94. info = fd.read().split('\n')
  95. return info
  96. def _get_release_tag():
  97. release_tag = "latest"
  98. if os.path.isfile("/etc/coriolis/coriolis.release"):
  99. with open("/etc/coriolis/coriolis.release") as fd:
  100. release_tag = fd.read().split('\n')[0]
  101. return release_tag
  102. def get_diagnostics_info():
  103. # TODO(gsamfira): decide if we want any other kind of
  104. # diagnostics.
  105. packages = list(freeze.freeze())
  106. return {
  107. "application": get_binary_name(),
  108. "packages": packages,
  109. "os_info": _get_host_os_info(),
  110. "hostname": get_hostname(),
  111. "ip_addresses": _get_local_ips(),
  112. "release_tag": _get_release_tag(),
  113. }
  114. def setup_logging():
  115. logging.setup(CONF, 'coriolis')
  116. def ignore_exceptions(func):
  117. @functools.wraps(func)
  118. def _ignore_exceptions(*args, **kwargs):
  119. try:
  120. return func(*args, **kwargs)
  121. except Exception:
  122. LOG.warn("Ignoring exception:\n%s", get_exception_details())
  123. return _ignore_exceptions
  124. def get_single_result(lis):
  125. """Indexes the head of a single element list.
  126. :raises KeyError: if the list is empty or its length is greater than 1.
  127. """
  128. if len(lis) == 0:
  129. raise KeyError("Result list is empty.")
  130. elif len(lis) > 1:
  131. raise KeyError("More than one result in list: '%s'" % lis)
  132. return lis[0]
  133. def retry_on_error(max_attempts=5, sleep_seconds=0,
  134. terminal_exceptions=[]):
  135. def _retry_on_error(func):
  136. @functools.wraps(func)
  137. def _exec_retry(*args, **kwargs):
  138. i = 0
  139. while True:
  140. try:
  141. return func(*args, **kwargs)
  142. except KeyboardInterrupt as ex:
  143. LOG.debug("Got a KeyboardInterrupt, skip retrying")
  144. LOG.exception(ex)
  145. raise
  146. except Exception as ex:
  147. if any([isinstance(ex, tex)
  148. for tex in terminal_exceptions]):
  149. raise
  150. i += 1
  151. if i < max_attempts:
  152. LOG.warn(
  153. "Exception occurred, retrying (%d/%d):\n%s",
  154. i, max_attempts, get_exception_details())
  155. time.sleep(sleep_seconds)
  156. else:
  157. raise
  158. return _exec_retry
  159. return _retry_on_error
  160. def get_udev_net_rules(net_ifaces_info):
  161. content = ""
  162. for name, mac_address in net_ifaces_info:
  163. content += ('SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", '
  164. 'ATTR{address}=="%(mac_address)s", NAME="%(name)s"\n' %
  165. {"name": name, "mac_address": mac_address.lower()})
  166. return content
  167. def parse_os_release(ssh):
  168. os_release_info = exec_ssh_cmd(
  169. ssh,
  170. "[ -f '/etc/os-release' ] && cat /etc/os-release || true").decode()
  171. info = {}
  172. for line in os_release_info.splitlines():
  173. if "=" not in line:
  174. continue
  175. k, v = line.split("=")
  176. k = k.strip()
  177. v = v.strip()
  178. info[k] = v.strip('"')
  179. if info.get("ID") and info.get("VERSION_ID"):
  180. return (info.get("ID"), info.get("VERSION_ID"))
  181. def parse_lsb_release(ssh):
  182. out = exec_ssh_cmd(ssh, "lsb_release -a || true").decode()
  183. dist_id = re.findall('^Distributor ID:\s(.*)$', out, re.MULTILINE)
  184. release = re.findall('^Release:\s(.*)$', out, re.MULTILINE)
  185. if dist_id and release:
  186. return (dist_id[0], release[0])
  187. def get_linux_os_info(ssh):
  188. info = parse_os_release(ssh)
  189. if info is None:
  190. # Fall back to lsb_release
  191. return parse_lsb_release(ssh)
  192. return info
  193. @retry_on_error()
  194. def test_ssh_path(ssh, remote_path):
  195. sftp = ssh.open_sftp()
  196. try:
  197. sftp.stat(remote_path)
  198. return True
  199. except IOError as ex:
  200. if ex.args[0] == 2:
  201. return False
  202. raise
  203. @retry_on_error()
  204. def read_ssh_file(ssh, remote_path):
  205. sftp = ssh.open_sftp()
  206. return sftp.open(remote_path, 'rb').read()
  207. @retry_on_error()
  208. def write_ssh_file(ssh, remote_path, content):
  209. sftp = ssh.open_sftp()
  210. fd = sftp.open(remote_path, 'wb')
  211. # Enabling pipelined transfers here will make
  212. # SFTP transfers much faster, but in combination
  213. # with eventlet, it seems to cause some lock-ups
  214. fd.write(content)
  215. fd.close()
  216. def write_winrm_file(conn, remote_path, content, overwrite=True):
  217. """This is a poor man's scp command that transfers small
  218. files, in chunks, over WinRM.
  219. """
  220. if conn.test_path(remote_path):
  221. if overwrite:
  222. conn.exec_ps_command(
  223. 'Remove-Item -Force "%s"' % remote_path)
  224. else:
  225. raise exception.CoriolisException(
  226. "File %s already exists" % remote_path)
  227. idx = 0
  228. while True:
  229. data = content[idx:idx + 2048]
  230. if not data:
  231. break
  232. if type(data) is str:
  233. data = data.encode()
  234. asb64 = base64.b64encode(data).decode()
  235. cmd = ("$ErrorActionPreference = 'Stop';"
  236. "$x = New-Object System.IO.FileStream(\"%s\", "
  237. "[System.IO.FileMode]::Append); $bytes = "
  238. "[Convert]::FromBase64String('%s'); $x.Write($bytes, "
  239. "0, $bytes.Length); $x.Close()") % (
  240. remote_path, asb64)
  241. conn.exec_ps_command(cmd)
  242. idx += 2048
  243. @retry_on_error()
  244. def list_ssh_dir(ssh, remote_path):
  245. sftp = ssh.open_sftp()
  246. return sftp.listdir(remote_path)
  247. @retry_on_error(terminal_exceptions=[exception.MinionMachineCommandTimeout])
  248. def exec_ssh_cmd(ssh, cmd, environment=None, get_pty=False, timeout=None):
  249. remote_str = "<undeterminable>"
  250. if timeout is not None:
  251. timeout = float(timeout)
  252. try:
  253. remote_str = "%s:%s" % ssh.get_transport().sock.getpeername()
  254. except (ValueError, AttributeError, TypeError):
  255. LOG.warn(
  256. "Failed to determine connection string for SSH connection: %s",
  257. get_exception_details())
  258. LOG.debug(
  259. "Executing the following SSH command on '%s' with "
  260. "environment %s: '%s'", remote_str, environment, cmd)
  261. _, stdout, stderr = ssh.exec_command(
  262. cmd, environment=environment, get_pty=get_pty, timeout=timeout)
  263. try:
  264. std_out = stdout.read()
  265. std_err = stderr.read()
  266. except socket.timeout:
  267. raise exception.MinionMachineCommandTimeout()
  268. exit_code = stdout.channel.recv_exit_status()
  269. if exit_code:
  270. raise exception.CoriolisException(
  271. "Command \"%s\" failed on host '%s' with exit code: %s\n"
  272. "stdout: %s\nstd_err: %s" %
  273. (cmd, remote_str, exit_code,
  274. std_out.decode(errors='ignore'),
  275. std_err.decode(errors='ignore')))
  276. # Most of the commands will use pseudo-terminal which unfortunately will
  277. # include a '\r' to every newline. This will affect all plugins too, so
  278. # best we can do now is replace them.
  279. return std_out.replace(b'\r\n', b'\n').replace(b'\n\r', b'\n')
  280. def exec_ssh_cmd_chroot(ssh, chroot_dir, cmd, environment=None, get_pty=False,
  281. timeout=None):
  282. return exec_ssh_cmd(ssh, "sudo -E chroot %s %s" % (chroot_dir, cmd),
  283. environment=environment, get_pty=get_pty,
  284. timeout=timeout)
  285. def check_fs(ssh, fs_type, dev_path):
  286. try:
  287. out = exec_ssh_cmd(
  288. ssh, "sudo fsck -p -t %s %s" % (fs_type, dev_path),
  289. get_pty=True).decode()
  290. LOG.debug("File system checked:\n%s", out)
  291. except Exception:
  292. LOG.warn("Checking file system returned an error:\n%s" % (
  293. get_exception_details()))
  294. raise
  295. def run_xfs_repair(ssh, dev_path):
  296. try:
  297. tmp_dir = exec_ssh_cmd(
  298. ssh, "mktemp -d").decode().rstrip("\n")
  299. LOG.debug("mounting %s on %s" % (dev_path, tmp_dir))
  300. mount_out = exec_ssh_cmd(
  301. ssh, "sudo mount %s %s" % (dev_path, tmp_dir),
  302. get_pty=True).decode()
  303. LOG.debug("mount returned: %s" % mount_out)
  304. LOG.debug("Umounting %s" % tmp_dir)
  305. umount_out = exec_ssh_cmd(
  306. ssh, "sudo umount %s" % tmp_dir, get_pty=True).decode()
  307. LOG.debug("umounting returned: %s" % umount_out)
  308. out = exec_ssh_cmd(
  309. ssh, "sudo xfs_repair %s" % dev_path, get_pty=True).decode()
  310. LOG.debug("File system repaired:\n%s", out)
  311. except Exception as ex:
  312. LOG.warn("xfs_repair returned an error:\n%s", str(ex))
  313. def _check_port_open(host, port):
  314. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  315. try:
  316. s.settimeout(1)
  317. s.connect((host, port))
  318. return True
  319. except (exception.ConnectionRefusedError, socket.timeout, OSError):
  320. return False
  321. finally:
  322. s.close()
  323. def wait_for_port_connectivity(address, port, max_wait=300):
  324. i = 0
  325. while not _check_port_open(address, port) and i < max_wait:
  326. time.sleep(1)
  327. i += 1
  328. if i == max_wait:
  329. raise exception.CoriolisException("Connection failed on port %s" %
  330. port)
  331. def exec_process(args):
  332. p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  333. std_out, std_err = p.communicate()
  334. if p.returncode:
  335. raise exception.CoriolisException(
  336. "Command \"%s\" failed with exit code: %s\nstdout: %s\nstd_err: %s"
  337. % (args, p.returncode, std_out, std_err))
  338. return std_out
  339. def get_disk_info(disk_path):
  340. out = exec_process([CONF.qemu_img_path, 'info', '--output=json',
  341. disk_path])
  342. disk_info = json.loads(out.decode())
  343. if disk_info["format"] == "vpc":
  344. disk_info["format"] = constants.DISK_FORMAT_VHD
  345. return disk_info
  346. def convert_disk_format(disk_path, target_disk_path, target_format,
  347. preallocated=False):
  348. allocation_args = []
  349. if preallocated:
  350. if target_format != constants.DISK_FORMAT_VHD:
  351. raise NotImplementedError(
  352. "Preallocation is supported only for the VHD format.")
  353. allocation_args = ['-o', 'subformat=fixed']
  354. if target_format == constants.DISK_FORMAT_VHD:
  355. target_format = "vpc"
  356. args = ([CONF.qemu_img_path, 'convert', '-O', target_format] +
  357. allocation_args +
  358. [disk_path, target_disk_path])
  359. try:
  360. exec_process(args)
  361. except Exception:
  362. ignore_exceptions(os.remove)(target_disk_path)
  363. raise
  364. def get_hostname():
  365. return socket.gethostname()
  366. def get_binary_name():
  367. return os.path.basename(sys.argv[0])
  368. def get_exception_details():
  369. return traceback.format_exc()
  370. def walk_class_hierarchy(clazz, encountered=None):
  371. """Walk class hierarchy, yielding most derived classes first."""
  372. if not encountered:
  373. encountered = []
  374. for subclass in clazz.__subclasses__():
  375. if subclass not in encountered:
  376. encountered.append(subclass)
  377. # drill down to leaves first
  378. for subsubclass in walk_class_hierarchy(subclass, encountered):
  379. yield subsubclass
  380. yield subclass
  381. def get_ssl_cert_thumbprint(context, host, port=443, digest_algorithm="sha1"):
  382. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  383. ssl_sock = context.wrap_socket(sock, server_hostname=host)
  384. ssl_sock.connect((host, port))
  385. # binary_form is the only option when the certificate is not validated
  386. cert = ssl_sock.getpeercert(binary_form=True)
  387. sock.close()
  388. x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, cert)
  389. return x509.digest(digest_algorithm).decode()
  390. def get_resources_dir():
  391. return os.path.join(
  392. os.path.dirname(os.path.abspath(__file__)), "resources")
  393. def get_resources_bin_dir():
  394. return os.path.join(get_resources_dir(), "bin")
  395. def serialize_key(key, password=None):
  396. key_io = io.StringIO()
  397. key.write_private_key(key_io, password)
  398. return key_io.getvalue()
  399. def deserialize_key(key_bytes, password=None):
  400. key_io = io.StringIO(key_bytes)
  401. return paramiko.RSAKey.from_private_key(key_io, password)
  402. def is_serializable(obj):
  403. pickle.dumps(obj)
  404. def to_dict(obj, max_depth=10):
  405. # jsonutils.dumps() has a max_depth of 3 by default
  406. def _to_primitive(value, convert_instances=False,
  407. convert_datetime=True, level=0,
  408. max_depth=max_depth):
  409. return jsonutils.to_primitive(
  410. value, convert_instances, convert_datetime, level, max_depth)
  411. return jsonutils.loads(jsonutils.dumps(obj, default=_to_primitive))
  412. def load_class(class_path):
  413. LOG.debug('Loading class \'%s\'' % class_path)
  414. parts = class_path.rsplit('.', 1)
  415. module = __import__(parts[0], fromlist=parts[1])
  416. return getattr(module, parts[1])
  417. def check_md5(data, md5):
  418. m = hashlib.md5()
  419. m.update(data)
  420. new_md5 = m.hexdigest()
  421. if new_md5 != md5:
  422. raise exception.CoriolisException("MD5 check failed")
  423. def get_secret_connection_info(ctxt, connection_info):
  424. secret_ref = connection_info.get("secret_ref")
  425. if secret_ref:
  426. LOG.info("Retrieving connection info from secret: %s", secret_ref)
  427. connection_info = secrets.get_secret(ctxt, secret_ref)
  428. return connection_info
  429. def parse_int_value(value):
  430. try:
  431. return int(str(value))
  432. except ValueError:
  433. raise exception.InvalidInput("Invalid integer: %s" % value)
  434. def decode_base64_param(value, is_json=False):
  435. try:
  436. decoded = base64.urlsafe_b64decode(value).decode()
  437. if is_json:
  438. decoded = json.loads(decoded)
  439. return decoded
  440. except (binascii.Error, TypeError, json.decoder.JSONDecodeError) as ex:
  441. raise exception.InvalidInput(reason=str(ex))
  442. def quote_url(text):
  443. return parse.quote(text.encode('UTF-8'), safe='')
  444. def normalize_mac_address(original_mac_address):
  445. """ Normalizez capitalized MAC addresses with or without '-' or ':'
  446. as separators into all-lower-case ':'-separated form. """
  447. if not isinstance(original_mac_address, str):
  448. raise ValueError(
  449. "MAC address must be a str, got type '%s': %s" % (
  450. type(original_mac_address), original_mac_address))
  451. res = ""
  452. mac_address = original_mac_address.strip().lower().replace('-', ':')
  453. if re.match(SPACED_MAC_ADDRESS_REGEX, mac_address):
  454. res = mac_address
  455. elif re.match(UNSPACED_MAC_ADDRESS_REGEX, mac_address):
  456. for i in range(0, len(mac_address), 2):
  457. res = "%s:%s" % (res, mac_address[i:i + 2])
  458. res = res.strip(':')
  459. if not re.match(SPACED_MAC_ADDRESS_REGEX, res):
  460. raise ValueError(
  461. "Failed to normalize MAC address '%s': ended up "
  462. "with: '%s'" % (original_mac_address, res))
  463. else:
  464. raise ValueError(
  465. "Improperly formatted MAC address: %s" % original_mac_address)
  466. LOG.debug(
  467. "Normalized MAC address '%s' to '%s'",
  468. original_mac_address, res)
  469. return res
  470. def get_url_with_credentials(url, username, password):
  471. parts = parse.urlsplit(url)
  472. # Remove previous credentials if set
  473. netloc = parts.netloc[parts.netloc.find('@') + 1:]
  474. netloc = "%s:%s@%s" % (
  475. quote_url(username), quote_url(password or ''), netloc)
  476. parts = parts._replace(netloc=netloc)
  477. return parse.urlunsplit(parts)
  478. def get_unique_option_ids(resources, id_key="id", name_key="name"):
  479. """Given a list of dictionaries with both the specified 'id_key' and
  480. 'name_key' in each, returns a list of strings, each identifying a certain
  481. dictionary thusly:
  482. - if the value under the 'name_key' of a dict is not unique among all
  483. others, returns the value of the 'id_key'
  484. - else, returns the value of the 'name_key
  485. """
  486. if not all([name_key in d and id_key in d for d in resources]):
  487. raise KeyError(
  488. "Some resources are missing the name key '%s' "
  489. "or ID key '%s': %s" % (name_key, id_key, resources))
  490. name_mappings = {}
  491. for resource in resources:
  492. if resource[name_key] in name_mappings:
  493. name_mappings[resource[name_key]].append(resource[id_key])
  494. else:
  495. name_mappings[resource[name_key]] = [resource[id_key]]
  496. identifiers = []
  497. for name, ids in name_mappings.items():
  498. # if it has only one id, it is unique, append name
  499. if len(ids) == 1:
  500. identifiers.append(name)
  501. else:
  502. # if it has multiple ids, append ids
  503. identifiers.extend(ids)
  504. return identifiers
  505. def bad_request_on_error(error_message):
  506. def _bad_request_on_error(func):
  507. def wrapper(*args, **kwargs):
  508. (is_valid, message) = func(*args, **kwargs)
  509. if not is_valid:
  510. raise exc.HTTPBadRequest(
  511. explanation=(error_message % message))
  512. return (is_valid, message)
  513. return wrapper
  514. return _bad_request_on_error
  515. def sanitize_task_info(task_info):
  516. """ Returns a copy of the given task info with any chunking
  517. info for volumes and sensitive credentials removed.
  518. """
  519. new = {}
  520. special_keys = ['volumes_info', 'origin', 'destination']
  521. for key in task_info.keys():
  522. if key not in special_keys:
  523. new[key] = copy.deepcopy(task_info[key])
  524. for key in ['origin', 'destination']:
  525. if key in task_info.keys():
  526. new[key] = copy.deepcopy(task_info[key])
  527. if type(new[key]) is dict and 'connection_info' in new[key]:
  528. new[key]['connection_info'] = {"got": "redacted"}
  529. if 'volumes_info' in task_info:
  530. new['volumes_info'] = []
  531. for vol in task_info['volumes_info']:
  532. vol_cpy = {}
  533. for key in vol:
  534. if key != "replica_state":
  535. vol_cpy[key] = copy.deepcopy(vol[key])
  536. else:
  537. vol_cpy['replica_state'] = {}
  538. for statekey in vol['replica_state']:
  539. if statekey != "chunks":
  540. vol_cpy['replica_state'][statekey] = (
  541. copy.deepcopy(
  542. vol['replica_state'][statekey]))
  543. else:
  544. vol_cpy['replica_state']["chunks"] = (
  545. ["<redacted>"])
  546. new['volumes_info'].append(vol_cpy)
  547. return new
  548. def parse_ini_config(file_contents):
  549. """ Parses the contents of the given .ini config file and
  550. returns a dict with the options/values within it.
  551. """
  552. config = {}
  553. regex_expr = '([^#]*[^-\\s#])\\s*=\\s*(?:"|\')?([^#"\']*)(?:"|\')?\\s*'
  554. for config_line in file_contents.splitlines():
  555. m = re.match(regex_expr, config_line)
  556. if m:
  557. name, value = m.groups()
  558. config[name] = value
  559. return config
  560. def read_ssh_ini_config_file(ssh, path, check_exists=False):
  561. """ Reads and parses the contents of an .ini file at the given path. """
  562. if not check_exists or test_ssh_path(ssh, path):
  563. content = read_ssh_file(ssh, path).decode()
  564. return parse_ini_config(content)
  565. else:
  566. return {}
  567. def _write_systemd(ssh, cmdline, svcname, run_as=None, start=True):
  568. serviceFilePath = "/lib/systemd/system/%s.service" % svcname
  569. if test_ssh_path(ssh, serviceFilePath):
  570. return
  571. def _reload_and_start(start=True):
  572. exec_ssh_cmd(
  573. ssh, "sudo systemctl daemon-reload",
  574. get_pty=True)
  575. if start:
  576. exec_ssh_cmd(
  577. ssh, "sudo systemctl start %s" % svcname,
  578. get_pty=True)
  579. def _correct_selinux_label():
  580. cmd = "sudo restorecon -v %s" % serviceFilePath
  581. try:
  582. exec_ssh_cmd(ssh, cmd, get_pty=True)
  583. except exception.CoriolisException:
  584. LOG.warn(
  585. "Could not relabel service '%s'. SELinux might not be "
  586. "installed. Error was: %s", svcname, get_exception_details())
  587. systemd_args = {
  588. "cmdline": cmdline,
  589. "username": "root",
  590. "svc_name": svcname,
  591. }
  592. if run_as:
  593. systemd_args["username"] = run_as
  594. systemdService = SYSTEMD_TEMPLATE % systemd_args
  595. name = str(uuid.uuid4())
  596. write_ssh_file(
  597. ssh, '/tmp/%s.service' % name, systemdService)
  598. exec_ssh_cmd(
  599. ssh,
  600. "sudo mv /tmp/%s.service %s" % (name, serviceFilePath),
  601. get_pty=True)
  602. _correct_selinux_label()
  603. _reload_and_start(start=start)
  604. def _write_upstart(ssh, cmdline, svcname, run_as=None, start=True):
  605. serviceFilePath = "/etc/init/%s.conf" % svcname
  606. if test_ssh_path(ssh, serviceFilePath):
  607. return
  608. if run_as:
  609. cmdline = "sudo -u %s -- %s" % (run_as, cmdline)
  610. upstartService = UPSTART_TEMPLATE % {
  611. "cmdline": cmdline,
  612. "svc_name": svcname,
  613. }
  614. name = str(uuid.uuid4())
  615. write_ssh_file(
  616. ssh, '/tmp/%s.conf' % name, upstartService)
  617. exec_ssh_cmd(
  618. ssh,
  619. "sudo mv /tmp/%s.conf %s" % (name, serviceFilePath),
  620. get_pty=True)
  621. if start:
  622. exec_ssh_cmd(ssh, "start %s" % svcname)
  623. @retry_on_error()
  624. def create_service(ssh, cmdline, svcname, run_as=None, start=True):
  625. # Simplistic check for init system. We usually use official images,
  626. # and none of the supported operating systems come with both upstart
  627. # and systemd installed side by side. So if /lib/systemd/system
  628. # exists, it's usually systemd enabled. If not, but /etc/init
  629. # exists, it's upstart
  630. if test_ssh_path(ssh, "/lib/systemd/system"):
  631. _write_systemd(ssh, cmdline, svcname, run_as=run_as, start=start)
  632. elif test_ssh_path(ssh, "/etc/init"):
  633. _write_upstart(ssh, cmdline, svcname, run_as=run_as, start=start)
  634. else:
  635. raise exception.CoriolisException(
  636. "could not determine init system")
  637. @retry_on_error()
  638. def restart_service(ssh, svcname):
  639. if test_ssh_path(ssh, "/lib/systemd/system"):
  640. exec_ssh_cmd(ssh, "sudo systemctl restart %s" % svcname, get_pty=True)
  641. elif test_ssh_path(ssh, "/etc/init"):
  642. exec_ssh_cmd(ssh, "restart %s" % svcname)
  643. else:
  644. raise exception.UnrecognizedWorkerInitSystem()
  645. @retry_on_error()
  646. def start_service(ssh, svcname):
  647. if test_ssh_path(ssh, "/lib/systemd/system"):
  648. exec_ssh_cmd(ssh, "sudo systemctl start %s" % svcname, get_pty=True)
  649. elif test_ssh_path(ssh, "/etc/init"):
  650. exec_ssh_cmd(ssh, "start %s" % svcname)
  651. else:
  652. raise exception.UnrecognizedWorkerInitSystem()
  653. @retry_on_error()
  654. def stop_service(ssh, svcname):
  655. if test_ssh_path(ssh, "/lib/systemd/system"):
  656. exec_ssh_cmd(ssh, "sudo systemctl stop %s" % svcname, get_pty=True)
  657. elif test_ssh_path(ssh, "/etc/init"):
  658. exec_ssh_cmd(ssh, "stop %s" % svcname)
  659. else:
  660. raise exception.UnrecognizedWorkerInitSystem()
  661. class Grub2ConfigEditor(object):
  662. """This class edits GRUB2 configs, normally found in
  663. /etc/default/grub. This class tries to preserve commented
  664. and empty lines.
  665. NOTE: This class does not actually write to file during
  666. commit. Rhather, it will mutate it's internal view of the
  667. contents of that file with the latest changes made.
  668. Use dump() to get the file contents.
  669. """
  670. def __init__(self, cfg):
  671. self._cfg = cfg
  672. self._parsed = self._parse_cfg(self._cfg)
  673. def _parse_cfg(self, cfg):
  674. ret = []
  675. for line in cfg.splitlines():
  676. if line.startswith("#") or len(line.strip()) == 0:
  677. ret.append(
  678. {
  679. "type": "raw",
  680. "payload": line
  681. }
  682. )
  683. continue
  684. vals = line.split("=", 1)
  685. if len(vals) != 2:
  686. ret.append(
  687. {
  688. "type": "raw",
  689. "payload": line
  690. }
  691. )
  692. continue
  693. quoted = False
  694. # should extend to single quotes
  695. if vals[1].startswith('"') and vals[1].endswith('"'):
  696. quoted = True
  697. vals[1] = vals[1].strip('"')
  698. if len(vals[1]) == 0 or vals[1][0] in string.punctuation:
  699. ret.append(
  700. {
  701. "type": "option",
  702. "payload": line,
  703. "quoted": quoted,
  704. "option_name": vals[0],
  705. "option_value": [
  706. {
  707. "opt_type": "single",
  708. "opt_val": vals[1],
  709. },
  710. ]
  711. }
  712. )
  713. continue
  714. val_sections = vals[1].split()
  715. opt_vals = []
  716. for sect in val_sections:
  717. fields = sect.split("=", 1)
  718. if len(fields) == 1:
  719. opt_vals.append(
  720. {
  721. "opt_type": "single",
  722. "opt_val": sect,
  723. }
  724. )
  725. else:
  726. opt_vals.append(
  727. {
  728. "opt_type": "key_val",
  729. "opt_val": fields[1],
  730. "opt_key": fields[0],
  731. }
  732. )
  733. ret.append(
  734. {
  735. "type": "option",
  736. "payload": line,
  737. "quoted": quoted,
  738. "option_name": vals[0],
  739. "option_value": opt_vals,
  740. }
  741. )
  742. return ret
  743. def _validate_value(self, value):
  744. if type(value) is not dict:
  745. raise ValueError("value was not dict")
  746. opt_type = value.get("opt_type")
  747. if opt_type not in ("key_val", "single"):
  748. raise ValueError("invalid value type %s" % opt_type)
  749. if opt_type == "key_val":
  750. if "opt_val" not in value or "opt_key" not in value:
  751. raise ValueError(
  752. "key_val option type requires "
  753. "opt_key key and opt_val")
  754. elif opt_type == "single":
  755. if "opt_val" not in value:
  756. raise ValueError(
  757. "single option type requires opt_val")
  758. else:
  759. raise ValueError("unknown option type: %s" % opt_type)
  760. def set_option(self, option, value):
  761. """Replaces the value of an option completely
  762. """
  763. self._validate_value(value)
  764. opt_found = False
  765. for opt in self._parsed:
  766. if opt.get("option_name") == option:
  767. opt_found = True
  768. opt["option_value"] = [value, ]
  769. break
  770. if not opt_found:
  771. self._parsed.append({
  772. "type": "option",
  773. "quoted": True,
  774. "option_name": option,
  775. "option_value": [
  776. value
  777. ],
  778. })
  779. def append_to_option(self, option, value):
  780. """Appends a value to the specified option. If we're passing
  781. in a key_val type and the option already exists, the value
  782. will be replaced. Options of type "single", if absent from the
  783. list, will be appended. If a single value already exists
  784. it will be ignored.
  785. """
  786. self._validate_value(value)
  787. opt_found = False
  788. for opt in self._parsed:
  789. if opt.get("option_name") == option:
  790. opt_found = True
  791. found = False
  792. for val in opt["option_value"]:
  793. if (val["opt_type"] == "key_val" and
  794. value["opt_type"] == "key_val"):
  795. if str(val["opt_key"]) == str(value["opt_key"]):
  796. val["opt_val"] = value["opt_val"]
  797. found = True
  798. elif (val["opt_type"] == "single" and
  799. value["opt_type"] == "single"):
  800. if str(val["opt_val"]) == str(value["opt_val"]):
  801. found = True
  802. if not found:
  803. opt["option_value"].append(value)
  804. break
  805. if not opt_found:
  806. self._parsed.append({
  807. "type": "option",
  808. "quoted": True,
  809. "option_name": option,
  810. "option_value": [
  811. value
  812. ],
  813. })
  814. def dump(self):
  815. """dumps the contents of the file"""
  816. tmp = StringIO()
  817. for line in self._parsed:
  818. if line["type"] == "raw":
  819. tmp.write("%s\n" % line["payload"])
  820. continue
  821. vals = line["option_value"]
  822. flat = []
  823. for val in vals:
  824. if val["opt_type"] == "key_val":
  825. flat.append("%s=%s" % (val["opt_key"], val["opt_val"]))
  826. else:
  827. flat.append(str(val["opt_val"]))
  828. if len(flat) == 0:
  829. tmp.write("%s=\n" % line["option_name"])
  830. continue
  831. val = " ".join(flat)
  832. quoted = line["quoted"]
  833. if len(flat) > 1:
  834. quoted = True
  835. fmt = '%s=%s' % (line["option_name"], val)
  836. if quoted:
  837. fmt = '%s="%s"' % (line["option_name"], val)
  838. tmp.write("%s\n" % fmt)
  839. tmp.seek(0)
  840. return tmp.read()