ResourcesManager.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. import getpass
  2. import logging
  3. import os
  4. import shutil
  5. import re
  6. from git import Repo
  7. from packaging import version
  8. from kalliope.core.ConfigurationManager import SettingLoader
  9. from kalliope.core.ConfigurationManager.DnaLoader import DnaLoader
  10. from kalliope.core.Models import Neuron
  11. from kalliope.core.NeuronLauncher import NeuronLauncher
  12. from kalliope.core.Utils import Utils
  13. logging.basicConfig()
  14. logger = logging.getLogger("kalliope")
  15. # Global values for processing:
  16. LOCAL_TMP_FOLDER = "/tmp/kalliope/resources/"
  17. TMP_GIT_FOLDER = "kalliope_new_module_temp_name"
  18. DNA_FILE_NAME = "dna.yml"
  19. INSTALL_FILE_NAME = "install.yml"
  20. # Global values for required parameters in DNA:
  21. DNA_NAME = "name"
  22. DNA_TYPE = "type"
  23. # Global_Names for 'types' to match:
  24. TYPE_NEURON = "neuron"
  25. TYPE_TTS = "tts"
  26. TYPE_STT = "stt"
  27. TYPE_TRIGGER = "trigger"
  28. class ResourcesManagerException(Exception):
  29. pass
  30. class ResourcesManager(object):
  31. def __init__(self, **kwargs):
  32. """
  33. This class is used to manage community resources.
  34. :param kwargs:
  35. git-url: the url of the module to clone and install
  36. """
  37. super(ResourcesManager, self).__init__()
  38. # get settings
  39. sl = SettingLoader()
  40. self.settings = sl.settings
  41. # in case of update or install, url where
  42. self.git_url = kwargs.get('git_url', None)
  43. # temp path where we install the new module
  44. self.tmp_path = LOCAL_TMP_FOLDER + TMP_GIT_FOLDER
  45. self.dna_file_path = self.tmp_path + os.sep + DNA_FILE_NAME
  46. self.install_file_path = self.tmp_path + os.sep + INSTALL_FILE_NAME
  47. self.dna = None
  48. def install(self):
  49. """
  50. Module installation method.
  51. """
  52. # first, we clone the repo
  53. self._clone_repo(path=self.tmp_path,
  54. git_url=self.git_url)
  55. # check the content of the cloned repo
  56. if self.is_repo_ok(dna_file_path=self.dna_file_path,
  57. install_file_path=self.install_file_path):
  58. # Load the dna.yml file
  59. self.dna = DnaLoader(self.dna_file_path).get_dna()
  60. if self.dna is not None:
  61. logger.debug("[ResourcesManager] DNA file content: " + str(self.dna))
  62. if self.is_settings_ok(resources=self.settings.resources, dna=self.dna):
  63. # the dna file is ok, check the supported version
  64. if self._check_supported_version(current_version=self.settings.kalliope_version,
  65. supported_versions=self.dna.kalliope_supported_version):
  66. # Let's find the target folder depending the type
  67. module_type = self.dna.module_type.lower()
  68. target_folder = self._get_target_folder(resources=self.settings.resources,
  69. module_type=module_type)
  70. if target_folder is not None:
  71. # let's move the tmp folder in the right folder and get a new path for the module
  72. module_name = self.dna.name.lower()
  73. target_path = self._rename_temp_folder(name=self.dna.name.lower(),
  74. target_folder=target_folder,
  75. tmp_path=self.tmp_path)
  76. # if the target_path exists, then run the install file within the new repository
  77. if target_path is not None:
  78. self.install_file_path = target_path + os.sep + INSTALL_FILE_NAME
  79. self.run_ansible_playbook_module(install_file_path=self.install_file_path)
  80. Utils.print_success("Module: %s installed" % module_name)
  81. else:
  82. logger.debug("[ResourcesManager] installation cancelled, deleting temp repo %s"
  83. % str(self.tmp_path))
  84. shutil.rmtree(self.tmp_path)
  85. @staticmethod
  86. def is_settings_ok(resources, dna):
  87. """
  88. Test if required settings files in config of Kalliope are ok.
  89. The resource object must not be empty
  90. Check id the use have set the an installation path in his settings for the target module type
  91. :param resources: the Resources model
  92. :param dna: DNA info about the module to install
  93. :return:
  94. """
  95. settings_ok = True
  96. if resources is None:
  97. message = "Resources folder not set in settings, cannot install."
  98. logger.debug(message)
  99. Utils.print_danger(message)
  100. settings_ok = False
  101. else:
  102. if dna.module_type == "neuron" and resources.neuron_folder is None:
  103. message = "Resources folder for neuron installation not set in settings, cannot install."
  104. logger.debug(message)
  105. Utils.print_danger(message)
  106. settings_ok = False
  107. if dna.module_type == "stt" and resources.stt_folder is None:
  108. message = "Resources folder for stt installation not set in settings, cannot install."
  109. logger.debug(message)
  110. Utils.print_danger(message)
  111. settings_ok = False
  112. if dna.module_type == "tts" and resources.tts_folder is None:
  113. message = "Resources folder for tts installation not set in settings, cannot install."
  114. logger.debug(message)
  115. Utils.print_danger(message)
  116. settings_ok = False
  117. if dna.module_type == "trigger" and resources.trigger_folder is None:
  118. message = "Resources folder for trigger installation not set in settings, cannot install."
  119. logger.debug(message)
  120. Utils.print_danger(message)
  121. settings_ok = False
  122. return settings_ok
  123. @staticmethod
  124. def is_repo_ok(dna_file_path, install_file_path):
  125. """
  126. Check if the git cloned repo is fine to be installed
  127. :return: True if repo is ok to be installed, False otherwise
  128. """
  129. Utils.print_info("Checking repository...")
  130. repo_ok = True
  131. # check that a install.yml file is present
  132. if not os.path.exists(install_file_path):
  133. Utils.print_danger("Missing %s file" % INSTALL_FILE_NAME)
  134. repo_ok = False
  135. if not os.path.exists(dna_file_path):
  136. Utils.print_danger("Missing %s file" % DNA_FILE_NAME)
  137. repo_ok = False
  138. return repo_ok
  139. @staticmethod
  140. def _get_target_folder(resources, module_type):
  141. """
  142. Return the folder from the resources and given a module type
  143. :param resources: Resource object
  144. :type resources: Resources
  145. :param module_type: type of the module
  146. :return: path of the folder
  147. """
  148. # dict to get the path behind a type of resource
  149. module_type_converter = {
  150. TYPE_NEURON: resources.neuron_folder,
  151. TYPE_STT: resources.stt_folder,
  152. TYPE_TTS: resources.tts_folder,
  153. TYPE_TRIGGER: resources.trigger_folder
  154. }
  155. # Let's find the right path depending of the type
  156. try:
  157. folder_path = module_type_converter[module_type]
  158. except KeyError:
  159. folder_path = None
  160. # No folder_path has been found
  161. message = "No %s folder set in settings, cannot install." % module_type
  162. if folder_path is None:
  163. logger.debug(message)
  164. Utils.print_danger(message)
  165. return folder_path
  166. @staticmethod
  167. def _clone_repo(path, git_url):
  168. """
  169. Use git to clone locally the neuron in a temp folder
  170. :return:
  171. """
  172. # clone the repo
  173. logger.debug("[ResourcesManager] GIT clone into folder: %s" % path)
  174. Utils.print_info("Cloning repository...")
  175. # if the folder already exist we remove it
  176. if os.path.exists(path):
  177. shutil.rmtree(path)
  178. else:
  179. os.makedirs(path)
  180. Repo.clone_from(git_url, path)
  181. @staticmethod
  182. def _rename_temp_folder(name, target_folder, tmp_path):
  183. """
  184. Rename the temp folder of the cloned repo
  185. Return the name of the path to install
  186. :return: path to install, None if already exists
  187. """
  188. logger.debug("[ResourcesManager] Rename temp folder")
  189. new_absolute_neuron_path = target_folder + os.sep + name
  190. try:
  191. os.rename(tmp_path, new_absolute_neuron_path)
  192. return new_absolute_neuron_path
  193. except OSError:
  194. # the folder already exist
  195. Utils.print_warning("The module %s already exist in the path %s" % (name, target_folder))
  196. # remove the cloned repo
  197. logger.debug("[ResourcesManager] Deleting temp folder %s" % str(tmp_path))
  198. shutil.rmtree(tmp_path)
  199. @staticmethod
  200. def run_ansible_playbook_module(install_file_path):
  201. """
  202. Run the install.yml file through an Ansible playbook using the dedicated neuron !
  203. :param install_file_path: the path of the Ansible playbook to run.
  204. :return:
  205. """
  206. logger.debug("[ResourcesManager] Run ansible playbook")
  207. Utils.print_info("Starting neuron installation")
  208. # ask the sudo password
  209. pswd = getpass.getpass('Sudo password:')
  210. ansible_neuron_parameters = {
  211. "task_file": install_file_path,
  212. "sudo": True,
  213. "sudo_user": "root",
  214. "sudo_password": pswd
  215. }
  216. neuron = Neuron(name="ansible_playbook", parameters=ansible_neuron_parameters)
  217. NeuronLauncher.start_neuron(neuron)
  218. @staticmethod
  219. def _check_supported_version(current_version, supported_versions):
  220. """
  221. The dna file contains supported Kalliope version for the module to install.
  222. Check if supported versions are match the current installed version. If not, ask the user to confirm the
  223. installation anyway
  224. :param current_version: current version installed of Kalliope. E.g 0.4.0
  225. :param supported_versions: list of supported version
  226. :return: True if the version is supported or user has confirmed the installation
  227. """
  228. logger.debug("[ResourcesManager] Current installed version of Kalliope: %s" % str(current_version))
  229. logger.debug("[ResourcesManager] Module supported version: %s" % str(supported_versions))
  230. supported_version_found = False
  231. # Extract major version
  232. match_current_version = re.search('^[\d]*[.][\d]*', current_version)
  233. if match_current_version:
  234. current_version = match_current_version.group(0)
  235. for supported_version in supported_versions:
  236. if version.parse(str(current_version)) == version.parse(str(supported_version)):
  237. # we found the exact version
  238. supported_version_found = True
  239. break
  240. if not supported_version_found:
  241. # we ask the user if we want to install the module even if the version doesn't match
  242. Utils.print_info("Current installed version of Kalliope: %s" % current_version)
  243. Utils.print_info("Module supported versions: %s" % str(supported_versions))
  244. Utils.print_warning("The neuron seems to be not supported by your current version of Kalliope")
  245. supported_version_found = Utils.query_yes_no("install it anyway?")
  246. logger.debug("[ResourcesManager] install it anyway user answer: %s" % supported_version_found)
  247. logger.debug("[ResourcesManager] check_supported_version: %s" % str(supported_version_found))
  248. return supported_version_found