utils.py 31 KB

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