2
0

utils.py 30 KB

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