helpers.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import fnmatch
  2. import functools
  3. import logging
  4. import os
  5. import re
  6. from collections.abc import Callable
  7. from collections.abc import Iterator
  8. from contextlib import contextmanager
  9. from typing import Any
  10. from typing import TypeVar
  11. from typing import cast
  12. from typing import overload
  13. from cryptography.hazmat.backends import default_backend
  14. from cryptography.hazmat.primitives import serialization as crypt_serialization
  15. from cryptography.hazmat.primitives.asymmetric import rsa
  16. from deprecation import deprecated
  17. import cloudbridge
  18. from ..interfaces.exceptions import InvalidParamException
  19. log = logging.getLogger(__name__)
  20. T = TypeVar("T")
  21. F = TypeVar("F", bound=Callable[..., Any])
  22. def generate_key_pair() -> tuple[str, str]:
  23. """
  24. This method generates a keypair and returns it as a tuple
  25. of (public, private) keys.
  26. The public key format is OpenSSH and private key format is PEM.
  27. """
  28. key_pair = rsa.generate_private_key(
  29. backend=default_backend(),
  30. public_exponent=65537,
  31. key_size=2048)
  32. private_key = key_pair.private_bytes(
  33. crypt_serialization.Encoding.PEM,
  34. crypt_serialization.PrivateFormat.PKCS8,
  35. crypt_serialization.NoEncryption()).decode('utf-8')
  36. public_key = key_pair.public_key().public_bytes(
  37. crypt_serialization.Encoding.OpenSSH,
  38. crypt_serialization.PublicFormat.OpenSSH).decode('utf-8')
  39. return public_key, private_key
  40. def filter_by(prop_name: str, kwargs: dict[str, Any],
  41. objs: list[T]) -> list[T]:
  42. """
  43. Utility method for filtering a list of objects by a property.
  44. If the given property has a non empty value in kwargs, then
  45. the list of objs is filtered by that value. Otherwise, the
  46. list of objs is returned as is.
  47. """
  48. prop_val = kwargs.pop(prop_name, None)
  49. if prop_val:
  50. if isinstance(prop_val, str):
  51. regex = fnmatch.translate(prop_val)
  52. results = [o for o in objs
  53. if getattr(o, prop_name)
  54. and re.search(regex, getattr(o, prop_name))]
  55. else:
  56. results = [o for o in objs
  57. if getattr(o, prop_name) == prop_val]
  58. return results
  59. else:
  60. return objs
  61. def generic_find(filter_names: list[str], kwargs: dict[str, Any],
  62. objs: list[T]) -> list[T]:
  63. """
  64. Utility method for filtering a list of objects by a list of filters.
  65. """
  66. matches = objs
  67. for name in filter_names:
  68. matches = filter_by(name, kwargs, matches)
  69. # All kwargs should have been popped at this time.
  70. if len(kwargs) > 0:
  71. raise InvalidParamException(
  72. "Unrecognised parameters for search: %s. Supported attributes: %s"
  73. % (kwargs, filter_names))
  74. return matches
  75. @contextmanager
  76. def cleanup_action(cleanup_func: Callable[[], object]) -> Iterator[None]:
  77. """
  78. Context manager to carry out a given
  79. cleanup action after carrying out a set
  80. of tasks, or when an exception occurs.
  81. If any errors occur during the cleanup
  82. action, those are ignored, and the original
  83. traceback is preserved.
  84. :params func: This function is called if
  85. an exception occurs or at the end of the
  86. context block. If any exceptions raised
  87. by func are ignored.
  88. Usage:
  89. with cleanup_action(lambda e: print("Oops!")):
  90. do_something()
  91. """
  92. try:
  93. yield
  94. except Exception:
  95. try:
  96. cleanup_func()
  97. except Exception:
  98. log.exception("Error during exception cleanup: ")
  99. raise
  100. try:
  101. cleanup_func()
  102. except Exception:
  103. log.exception("Error during exception cleanup: ")
  104. @overload
  105. def get_env(varname: str) -> str | None:
  106. ...
  107. @overload
  108. def get_env(varname: str, default_value: T) -> str | T:
  109. ...
  110. def get_env(varname: str, default_value: object = None) -> object:
  111. """
  112. Return the value of the environment variable or default_value.
  113. :type varname: ``str``
  114. :param varname: Name of the environment variable for which to check.
  115. :param default_value: Return this value is the env var is not found.
  116. Defaults to ``None``.
  117. :return: Value of the supplied environment if found; value of
  118. ``default_value`` otherwise.
  119. """
  120. return os.environ.get(varname, default_value)
  121. # Alias deprecation decorator, following:
  122. # https://stackoverflow.com/questions/49802412/
  123. # how-to-implement-deprecation-in-python-with-argument-alias
  124. def deprecated_alias(**aliases: str) -> Callable[[F], F]:
  125. def deco(f: F) -> F:
  126. @functools.wraps(f)
  127. def wrapper(*args: Any, **kwargs: Any) -> Any:
  128. rename_kwargs(f.__name__, kwargs, aliases)
  129. return f(*args, **kwargs)
  130. return cast(F, wrapper)
  131. return deco
  132. def rename_kwargs(func_name: str, kwargs: dict[str, Any],
  133. aliases: dict[str, str]) -> None:
  134. for alias, new in aliases.items():
  135. if alias in kwargs:
  136. if new in kwargs:
  137. raise InvalidParamException(
  138. '{} received both {} and {}'.format(func_name, alias, new))
  139. # Manually invoke the deprecated decorator with an empty lambda
  140. # to signal deprecation
  141. deprecated(deprecated_in='1.1',
  142. removed_in='2.0',
  143. current_version=cloudbridge.__version__,
  144. details='{} is deprecated, use {} instead'.format(
  145. alias, new))(lambda: None)()
  146. kwargs[new] = kwargs.pop(alias)
  147. NON_ALPHA_NUM = re.compile(r"[^A-Za-z0-9]+")
  148. def to_resource_name(value: str, replace_with: str = "-") -> str:
  149. """
  150. Converts a given string to a valid resource name by stripping
  151. all characters that are not alphanumeric.
  152. :param value: the value to strip
  153. :param replace_with: the value to replace mismatching characters with
  154. :return: a string with all mismatching characters removed.
  155. """
  156. val = re.sub(NON_ALPHA_NUM, replace_with, value)
  157. return val.strip("-")