PageRenderTime 46ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/slapos/recipe/librecipe/__init__.py

https://github.com/SlapOS/slapos
Python | 330 lines | 211 code | 36 blank | 83 comment | 17 complexity | e1170872717be40ad3cf9e228fb5c80a MD5 | raw file
  1. ##############################################################################
  2. #
  3. # Copyright (c) 2010 Vifib SARL and Contributors. All Rights Reserved.
  4. #
  5. # WARNING: This program as such is intended to be used by professional
  6. # programmers who take the whole responsibility of assessing all potential
  7. # consequences resulting from its eventual inadequacies and bugs
  8. # End users who are looking for a ready-to-use solution with commercial
  9. # guarantees and support are strongly adviced to contract a Free Software
  10. # Service Company
  11. #
  12. # This program is Free Software; you can redistribute it and/or
  13. # modify it under the terms of the GNU General Public License
  14. # as published by the Free Software Foundation; either version 3
  15. # of the License, or (at your option) any later version.
  16. #
  17. # This program is distributed in the hope that it will be useful,
  18. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. # GNU General Public License for more details.
  21. #
  22. # You should have received a copy of the GNU General Public License
  23. # along with this program; if not, write to the Free Software
  24. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  25. #
  26. ##############################################################################
  27. import logging
  28. from slapos import slap
  29. import os
  30. import zc.buildout
  31. import zc.recipe.egg
  32. from hashlib import md5
  33. import stat
  34. import netaddr
  35. import time
  36. import re
  37. from six.moves.urllib.parse import urlunparse
  38. import json
  39. # Use to do from slapos.recipe.librecipe import GenericBaseRecipe
  40. from .generic import GenericBaseRecipe
  41. from .genericslap import GenericSlapRecipe
  42. from .filehash import filehash, generateHashFromFiles
  43. # Utility functions to (de)serialise live python objects in order to send them
  44. # to master.
  45. JSON_SERIALISED_MAGIC_KEY = '_'
  46. def wrap(value):
  47. return {JSON_SERIALISED_MAGIC_KEY: json.dumps(value)}
  48. def unwrap(value):
  49. try:
  50. value = value[JSON_SERIALISED_MAGIC_KEY]
  51. except (KeyError, TypeError):
  52. pass
  53. else:
  54. value = json.loads(value)
  55. return value
  56. class BaseSlapRecipe:
  57. """Base class for all slap.recipe.*"""
  58. def __init__(self, buildout, name, options):
  59. """Default initialisation"""
  60. self.name = name
  61. options['eggs'] = 'slapos.cookbook'
  62. self.options = options
  63. self.logger = logging.getLogger(self.name)
  64. self.slap = slap.slap()
  65. self.work_directory = os.path.abspath(buildout['buildout'][
  66. 'directory'])
  67. self.bin_directory = os.path.join(buildout['buildout'][
  68. 'directory'], 'bin')
  69. self.data_root_directory = os.path.join(self.work_directory, 'srv')
  70. self.backup_directory = os.path.join(self.data_root_directory, 'backup')
  71. self.var_directory = os.path.join(self.work_directory, 'var')
  72. self.log_directory = os.path.join(self.var_directory, 'log')
  73. self.run_directory = os.path.join(self.var_directory, 'run')
  74. self.etc_directory = os.path.join(self.work_directory, 'etc')
  75. self.tmp_directory = os.path.join(self.work_directory, 'tmp')
  76. self.wrapper_directory = os.path.join(self.etc_directory, 'run')
  77. self.wrapper_report_directory = os.path.join(self.etc_directory, 'report')
  78. self.wrapper_xml_report_directory = os.path.join(self.var_directory,
  79. 'xml_report')
  80. self.destroy_script_location = os.path.join(self, self.work_directory,
  81. 'sbin', 'destroy')
  82. self.promise_directory = os.path.join(self.etc_directory, 'promise')
  83. # default directory structure information
  84. self.default_directory_list = [
  85. self.bin_directory, # CP/bin - instance own binaries
  86. os.path.join(self, self.work_directory, 'sbin'), # CP/sbin - system
  87. # binaries, not exposed, only CP/sbin/destroy
  88. self.data_root_directory, # CP/srv - data container
  89. self.backup_directory, # CP/srv/backup - backup container
  90. self.etc_directory, # CP/etc - configuration container
  91. self.wrapper_directory, # CP/etc/run - for wrappers
  92. self.wrapper_report_directory, # CP/etc/report - for report wrappers
  93. self.promise_directory, # CP/etc/promise - for promise checking scripts
  94. self.var_directory, # CP/var - partition "internal" container for logs,
  95. # and another metadata
  96. self.wrapper_xml_report_directory, # CP/var/xml_report - for xml_report wrappers
  97. self.log_directory, # CP/var/log - log container
  98. self.run_directory, # CP/var/run - working container - pids, sockets
  99. self.tmp_directory, # CP/tmp - temporary files
  100. ]
  101. # SLAP related information
  102. try:
  103. slap_connection = buildout['slap_connection']
  104. self.computer_id = slap_connection['computer_id']
  105. self.computer_partition_id = slap_connection['partition_id']
  106. self.server_url = slap_connection['server_url']
  107. self.software_release_url = slap_connection['software_release_url']
  108. self.key_file = slap_connection.get('key_file')
  109. self.cert_file = slap_connection.get('cert_file')
  110. except zc.buildout.buildout.MissingSection:
  111. slap_connection = buildout['slap-connection']
  112. self.computer_id = slap_connection['computer-id']
  113. self.computer_partition_id = slap_connection['partition-id']
  114. self.server_url = slap_connection['server-url']
  115. self.software_release_url = slap_connection['software-release-url']
  116. self.key_file = slap_connection.get('key-file')
  117. self.cert_file = slap_connection.get('cert-file')
  118. # setup egg to give possibility to generate scripts
  119. self.egg = zc.recipe.egg.Egg(buildout, options['recipe'], options)
  120. # Hook options
  121. self._options(options)
  122. # setup auto uninstall/install
  123. self._setupAutoInstallUninstall()
  124. def _setupAutoInstallUninstall(self):
  125. """By default SlapOS recipes are reinstalled each time"""
  126. # Note: It is possible to create in future subclass which will do no-op in
  127. # this method
  128. self.options['slapos_timestamp'] = str(time.time())
  129. def _getIpAddress(self, test_method):
  130. """Internal helper method to fetch ip address"""
  131. if not 'ip_list' in self.parameter_dict:
  132. raise AttributeError
  133. for name, ip in self.parameter_dict['ip_list']:
  134. if test_method(ip):
  135. return ip
  136. raise AttributeError
  137. def getLocalIPv4Address(self):
  138. """Returns local IPv4 address available on partition"""
  139. # XXX: Lack checking for locality of address
  140. return self._getIpAddress(netaddr.valid_ipv4)
  141. def getGlobalIPv6Address(self):
  142. """Returns global IPv6 address available on partition"""
  143. # XXX: Lack checking for globality of address
  144. return self._getIpAddress(netaddr.valid_ipv6)
  145. def createConfigurationFile(self, name, content):
  146. """Creates named configuration file and returns its path"""
  147. file_path = os.path.join(self.etc_directory, name)
  148. self._writeFile(file_path, content)
  149. self.logger.debug('Created configuration file: %r' % file_path)
  150. return file_path
  151. def createRunningWrapper(self, wrapper_name, file_content):
  152. """Creates named running wrapper and returns its path"""
  153. wrapper_path = os.path.join(self.wrapper_directory, wrapper_name)
  154. self._writeExecutable(wrapper_path, file_content)
  155. return wrapper_path
  156. def substituteTemplate(self, template_location, mapping_dict):
  157. """Returns template content after substitution"""
  158. return open(template_location, 'r').read() % mapping_dict
  159. def _writeExecutable(self, path, content, mode='0700'):
  160. """Creates file in path with content and sets mode
  161. If file was created or altered returns true
  162. Otherwise returns false
  163. To be used to create executables
  164. Raises os related errors"""
  165. return self._writeFile(path, content, mode)
  166. def _writeFile(self, path, content, mode='0600'):
  167. """Creates file in path with content and sets mode
  168. If file was created or altered returns true
  169. Otherwise returns false
  170. Raises os related errors"""
  171. file_altered = False
  172. if not os.path.exists(path):
  173. open(path, 'w').write(content)
  174. file_altered = True
  175. else:
  176. new_sum = md5()
  177. current_sum = md5()
  178. new_sum.update(content)
  179. current_sum.update(open(path, 'r').read())
  180. if new_sum.digest() != current_sum.digest():
  181. file_altered = True
  182. open(path, 'w').write(content)
  183. if oct(stat.S_IMODE(os.stat(path).st_mode)) != mode:
  184. os.chmod(path, int(mode, 8))
  185. file_altered = True
  186. return file_altered
  187. def createBackupDirectory(self, name, mode='0700'):
  188. """Creates named directory in self.backup_directory and returns its path"""
  189. path = os.path.join(self.backup_directory, name)
  190. self._createDirectory(path, mode)
  191. return path
  192. def createDataDirectory(self, name, mode='0700'):
  193. """Creates named directory in self.data_root_directory and returns its path"""
  194. path = os.path.join(self.data_root_directory, name)
  195. self._createDirectory(path, mode)
  196. return path
  197. def _createDirectory(self, path, mode='0700'):
  198. """Creates path directory and sets mode
  199. If directory was created or its mode was altered returns true
  200. Otherwise returns false
  201. Raises os related errors"""
  202. directory_altered = False
  203. if not os.path.exists(path):
  204. os.mkdir(path, int(mode, 8))
  205. directory_altered = True
  206. if not os.path.isdir(path):
  207. raise zc.buildout.UserError('Path %r exists, but it is not directory'
  208. % path)
  209. if oct(stat.S_IMODE(os.stat(path).st_mode)) != mode:
  210. os.chmod(path, int(mode, 8))
  211. directory_altered = True
  212. if directory_altered:
  213. self.logger.debug('Created directory %r with permission %r' % (path, mode))
  214. return directory_altered
  215. def _createDefaultDirectoryStructure(self):
  216. for directory in self.default_directory_list:
  217. self._createDirectory(directory)
  218. def generatePassword(self, len=32):
  219. """Generates password. Shall be secured, until then all are insecure"""
  220. return 'insecure'
  221. def install(self):
  222. self.logger.warning("BaseSlapRecipe has been deprecated. Use " \
  223. "GenericBaseRecipe or GenericSlapRecipe instead.")
  224. self.slap.initializeConnection(self.server_url, self.key_file,
  225. self.cert_file)
  226. self.computer_partition = self.slap.registerComputerPartition(
  227. self.computer_id,
  228. self.computer_partition_id)
  229. self.request = self.computer_partition.request
  230. self.setConnectionDict = self.computer_partition.setConnectionDict
  231. self._createDefaultDirectoryStructure()
  232. self.parameter_dict = self.computer_partition.getInstanceParameterDict()
  233. # call children part of install
  234. path_list = self._install()
  235. return path_list
  236. update = install
  237. def _install(self):
  238. """Hook which shall be implemented in children class"""
  239. raise NotImplementedError('Shall be implemented by subclass')
  240. def _options(self, options):
  241. """Hook which can be implemented in children class"""
  242. pass
  243. def createPromiseWrapper(self, promise_name, file_content):
  244. """Create a promise wrapper.
  245. This wrapper aim to check if the software release is doing its job.
  246. Return the promise file path.
  247. """
  248. promise_path = os.path.join(self.promise_directory, promise_name)
  249. self._writeExecutable(promise_path, file_content)
  250. return promise_path
  251. def setConnectionUrl(self, *args, **kwargs):
  252. url = self._unparseUrl(*args, **kwargs)
  253. self.setConnectionDict(dict(url=url))
  254. def _unparseUrl(self, scheme, host, path='', params='', query='',
  255. fragment='', port=None, auth=None):
  256. """Join a url with auth, host, and port.
  257. * auth can be either a login string or a tuple (login, password).
  258. * if the host is an ipv6 address, brackets will be added to surround it.
  259. """
  260. # XXX-Antoine: I didn't find any standard module to join an url with
  261. # login, password, ipv6 host and port.
  262. # So instead of copy and past in every recipe I factorized it right here.
  263. netloc = ''
  264. if auth is not None:
  265. auth = tuple(auth)
  266. netloc = str(auth[0]) # Login
  267. if len(auth) > 1:
  268. netloc += ':%s' % auth[1] # Password
  269. netloc += '@'
  270. # host is an ipv6 address whithout brackets
  271. if ':' in host and not re.match(r'^\[.*\]$', host):
  272. netloc += '[%s]' % host
  273. else:
  274. netloc += str(host)
  275. if port is not None:
  276. netloc += ':%s' % port
  277. url = urlunparse((scheme, netloc, path, params, query, fragment))
  278. return url