PageRenderTime 25ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/docker/base/set_configs.py

https://gitlab.com/unofficial-mirrors/openstack-kolla
Python | 422 lines | 328 code | 63 blank | 31 comment | 77 complexity | c14de379b498b26bdac263f91aa1f6e1 MD5 | raw file
  1. #!/usr/bin/env python
  2. # Licensed under the Apache License, Version 2.0 (the "License");
  3. # you may not use this file except in compliance with the License.
  4. # You may obtain a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software
  9. # distributed under the License is distributed on an "AS IS" BASIS,
  10. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. # See the License for the specific language governing permissions and
  12. # limitations under the License.
  13. import argparse
  14. import glob
  15. import grp
  16. import json
  17. import logging
  18. import os
  19. import pwd
  20. import shutil
  21. import sys
  22. # TODO(rhallisey): add docstring.
  23. logging.basicConfig()
  24. LOG = logging.getLogger(__name__)
  25. LOG.setLevel(logging.INFO)
  26. class ExitingException(Exception):
  27. def __init__(self, message, exit_code=1):
  28. super(ExitingException, self).__init__(message)
  29. self.exit_code = exit_code
  30. class ImmutableConfig(ExitingException):
  31. pass
  32. class InvalidConfig(ExitingException):
  33. pass
  34. class MissingRequiredSource(ExitingException):
  35. pass
  36. class UserNotFound(ExitingException):
  37. pass
  38. class ConfigFileBadState(ExitingException):
  39. pass
  40. class ConfigFile(object):
  41. def __init__(self, source, dest, owner=None, perm=None, optional=False,
  42. preserve_properties=False, merge=False):
  43. self.source = source
  44. self.dest = dest
  45. self.owner = owner
  46. self.perm = perm
  47. self.optional = optional
  48. self.merge = merge
  49. self.preserve_properties = preserve_properties
  50. def __str__(self):
  51. return '<ConfigFile source:"{}" dest:"{}">'.format(self.source,
  52. self.dest)
  53. def _copy_file(self, source, dest):
  54. self._delete_path(dest)
  55. # dest endswith / means copy the <source> to <dest> folder
  56. LOG.info('Copying %s to %s', source, dest)
  57. if self.merge and self.preserve_properties and os.path.islink(source):
  58. link_target = os.readlink(source)
  59. os.symlink(link_target, dest)
  60. else:
  61. shutil.copy(source, dest)
  62. self._set_properties(source, dest)
  63. def _merge_directories(self, source, dest):
  64. if os.path.isdir(source):
  65. if os.path.lexists(dest) and not os.path.isdir(dest):
  66. self._delete_path(dest)
  67. if not os.path.isdir(dest):
  68. LOG.info('Creating directory %s', dest)
  69. os.makedirs(dest)
  70. self._set_properties(source, dest)
  71. dir_content = os.listdir(source)
  72. for to_copy in dir_content:
  73. self._merge_directories(os.path.join(source, to_copy),
  74. os.path.join(dest, to_copy))
  75. else:
  76. self._copy_file(source, dest)
  77. def _delete_path(self, path):
  78. if not os.path.lexists(path):
  79. return
  80. LOG.info('Deleting %s', path)
  81. if os.path.isdir(path):
  82. shutil.rmtree(path)
  83. else:
  84. os.remove(path)
  85. def _create_parent_dirs(self, path):
  86. parent_path = os.path.dirname(path)
  87. if not os.path.exists(parent_path):
  88. os.makedirs(parent_path)
  89. def _set_properties(self, source, dest):
  90. if self.preserve_properties:
  91. self._set_properties_from_file(source, dest)
  92. else:
  93. self._set_properties_from_conf(dest)
  94. def _set_properties_from_file(self, source, dest):
  95. shutil.copystat(source, dest)
  96. stat = os.stat(source)
  97. os.chown(dest, stat.st_uid, stat.st_gid)
  98. def _set_properties_from_conf(self, path):
  99. config = {'permissions':
  100. [{'owner': self.owner, 'path': path, 'perm': self.perm}]}
  101. handle_permissions(config)
  102. def copy(self):
  103. sources = glob.glob(self.source)
  104. if not self.optional and not sources:
  105. raise MissingRequiredSource('%s file is not found' % self.source)
  106. # skip when there is no sources and optional
  107. elif self.optional and not sources:
  108. return
  109. for source in sources:
  110. dest = self.dest
  111. # dest endswith / means copy the <source> into <dest> folder,
  112. # otherwise means copy the source to dest
  113. if dest.endswith(os.sep):
  114. dest = os.path.join(dest, os.path.basename(source))
  115. if not self.merge:
  116. self._delete_path(dest)
  117. self._create_parent_dirs(dest)
  118. self._merge_directories(source, dest)
  119. def _cmp_file(self, source, dest):
  120. # check exsit
  121. if (os.path.exists(source) and
  122. not self.optional and
  123. not os.path.exists(dest)):
  124. return False
  125. # check content
  126. with open(source) as f1, open(dest) as f2:
  127. if f1.read() != f2.read():
  128. LOG.error('The content of source file(%s) and'
  129. ' dest file(%s) are not equal.', source, dest)
  130. return False
  131. # check perm
  132. file_stat = os.stat(dest)
  133. actual_perm = oct(file_stat.st_mode)[-4:]
  134. if self.perm != actual_perm:
  135. LOG.error('Dest file does not have expected perm: %s, actual: %s',
  136. self.perm, actual_perm)
  137. return False
  138. # check owner
  139. desired_user, desired_group = user_group(self.owner)
  140. actual_user = pwd.getpwuid(file_stat.st_uid)
  141. if actual_user.pw_name != desired_user:
  142. LOG.error('Dest file does not have expected user: %s,'
  143. ' actual: %s ', desired_user, actual_user.pw_name)
  144. return False
  145. actual_group = grp.getgrgid(file_stat.st_gid)
  146. if actual_group.gr_name != desired_group:
  147. LOG.error('Dest file does not have expected group: %s,'
  148. ' actual: %s ', desired_group, actual_group.gr_name)
  149. return False
  150. return True
  151. def _cmp_dir(self, source, dest):
  152. for root, dirs, files in os.walk(source):
  153. for dir_ in dirs:
  154. full_path = os.path.join(root, dir_)
  155. dest_full_path = os.path.join(dest, os.path.relpath(source,
  156. full_path))
  157. dir_stat = os.stat(dest_full_path)
  158. actual_perm = oct(dir_stat.st_mode)[-4:]
  159. if self.perm != actual_perm:
  160. LOG.error('Dest dir does not have expected perm: %s,'
  161. ' actual %s', self.perm, actual_perm)
  162. return False
  163. for file_ in files:
  164. full_path = os.path.join(root, file_)
  165. dest_full_path = os.path.join(dest, os.path.relpath(source,
  166. full_path))
  167. if not self._cmp_file(full_path, dest_full_path):
  168. return False
  169. return True
  170. def check(self):
  171. bad_state_files = []
  172. sources = glob.glob(self.source)
  173. if not sources and not self.optional:
  174. raise MissingRequiredSource('%s file is not found' % self.source)
  175. elif self.optional and not sources:
  176. return
  177. for source in sources:
  178. dest = self.dest
  179. # dest endswith / means copy the <source> into <dest> folder,
  180. # otherwise means copy the source to dest
  181. if dest.endswith(os.sep):
  182. dest = os.path.join(dest, os.path.basename(source))
  183. if os.path.isdir(source) and not self._cmp_dir(source, dest):
  184. bad_state_files.append(source)
  185. elif not self._cmp_file(source, dest):
  186. bad_state_files.append(source)
  187. if len(bad_state_files) != 0:
  188. msg = 'Following files are in bad state: %s' % bad_state_files
  189. raise ConfigFileBadState(msg)
  190. def validate_config(config):
  191. required_keys = {'source', 'dest'}
  192. if 'command' not in config:
  193. raise InvalidConfig('Config is missing required "command" key')
  194. # Validate config sections
  195. for data in config.get('config_files', list()):
  196. # Verify required keys exist.
  197. if not data.viewkeys() >= required_keys:
  198. message = 'Config is missing required keys: %s' % required_keys
  199. raise InvalidConfig(message)
  200. if ('owner' not in data or 'perm' not in data) \
  201. and not data.get('preserve_properties', False):
  202. raise InvalidConfig(
  203. 'Config needs preserve_properties or owner and perm')
  204. def validate_source(data):
  205. source = data.get('source')
  206. # Only check existence if no wildcard found
  207. if '*' not in source:
  208. if not os.path.exists(source):
  209. if data.get('optional'):
  210. LOG.info("%s does not exist, but is not required", source)
  211. return False
  212. else:
  213. raise MissingRequiredSource(
  214. "The source to copy does not exist: %s" % source)
  215. return True
  216. def load_config():
  217. def load_from_env():
  218. config_raw = os.environ.get("KOLLA_CONFIG")
  219. if config_raw is None:
  220. return None
  221. # Attempt to read config
  222. try:
  223. return json.loads(config_raw)
  224. except ValueError:
  225. raise InvalidConfig('Invalid json for Kolla config')
  226. def load_from_file():
  227. config_file = os.environ.get("KOLLA_CONFIG_FILE")
  228. if not config_file:
  229. config_file = '/var/lib/kolla/config_files/config.json'
  230. LOG.info("Loading config file at %s", config_file)
  231. # Attempt to read config file
  232. with open(config_file) as f:
  233. try:
  234. return json.load(f)
  235. except ValueError:
  236. raise InvalidConfig(
  237. "Invalid json file found at %s" % config_file)
  238. except IOError as e:
  239. raise InvalidConfig(
  240. "Could not read file %s: %r" % (config_file, e))
  241. config = load_from_env()
  242. if config is None:
  243. config = load_from_file()
  244. LOG.info('Validating config file')
  245. validate_config(config)
  246. return config
  247. def copy_config(config):
  248. if 'config_files' in config:
  249. LOG.info('Copying service configuration files')
  250. for data in config['config_files']:
  251. config_file = ConfigFile(**data)
  252. config_file.copy()
  253. else:
  254. LOG.debug('No files to copy found in config')
  255. LOG.info('Writing out command to execute')
  256. LOG.debug("Command is: %s", config['command'])
  257. # The value from the 'command' key will be written to '/run_command'
  258. with open('/run_command', 'w+') as f:
  259. f.write(config['command'])
  260. def user_group(owner):
  261. if ':' in owner:
  262. user, group = owner.split(':', 1)
  263. if not group:
  264. group = user
  265. else:
  266. user, group = owner, owner
  267. return user, group
  268. def handle_permissions(config):
  269. for permission in config.get('permissions', list()):
  270. path = permission.get('path')
  271. owner = permission.get('owner')
  272. recurse = permission.get('recurse', False)
  273. perm = permission.get('perm')
  274. desired_user, desired_group = user_group(owner)
  275. uid = pwd.getpwnam(desired_user).pw_uid
  276. gid = grp.getgrnam(desired_group).gr_gid
  277. def set_perms(path, uid, gid, perm):
  278. LOG.info('Setting permission for %s', path)
  279. if not os.path.exists(path):
  280. LOG.warning('%s does not exist', path)
  281. return
  282. try:
  283. os.chown(path, uid, gid)
  284. except OSError:
  285. LOG.exception('Failed to change ownership of %s to %s:%s',
  286. path, uid, gid)
  287. if perm:
  288. # NOTE(Jeffrey4l): py3 need '0oXXX' format for octal literals,
  289. # and py2 support such format too.
  290. if len(perm) == 4 and perm[1] != 'o':
  291. perm = ''.join([perm[:1], 'o', perm[1:]])
  292. perm = int(perm, base=0)
  293. try:
  294. os.chmod(path, perm)
  295. except OSError:
  296. LOG.exception('Failed to set permission of %s to %s',
  297. path, perm)
  298. for dest in glob.glob(path):
  299. set_perms(dest, uid, gid, perm)
  300. if recurse and os.path.isdir(dest):
  301. for root, dirs, files in os.walk(dest):
  302. for dir_ in dirs:
  303. set_perms(os.path.join(root, dir_), uid, gid, perm)
  304. for file_ in files:
  305. set_perms(os.path.join(root, file_), uid, gid, perm)
  306. def execute_config_strategy(config):
  307. config_strategy = os.environ.get("KOLLA_CONFIG_STRATEGY")
  308. LOG.info("Kolla config strategy set to: %s", config_strategy)
  309. if config_strategy == "COPY_ALWAYS":
  310. copy_config(config)
  311. handle_permissions(config)
  312. elif config_strategy == "COPY_ONCE":
  313. if os.path.exists('/configured'):
  314. raise ImmutableConfig(
  315. "The config strategy prevents copying new configs",
  316. exit_code=0)
  317. else:
  318. copy_config(config)
  319. handle_permissions(config)
  320. os.mknod('/configured')
  321. else:
  322. raise InvalidConfig('KOLLA_CONFIG_STRATEGY is not set properly')
  323. def execute_config_check(config):
  324. for data in config['config_files']:
  325. config_file = ConfigFile(**data)
  326. config_file.check()
  327. def main():
  328. try:
  329. parser = argparse.ArgumentParser()
  330. parser.add_argument('--check',
  331. action='store_true',
  332. required=False,
  333. help='Check whether the configs changed')
  334. args = parser.parse_args()
  335. config = load_config()
  336. if args.check:
  337. execute_config_check(config)
  338. else:
  339. execute_config_strategy(config)
  340. except ExitingException as e:
  341. LOG.error("%s: %s", e.__class__.__name__, e)
  342. return e.exit_code
  343. except Exception:
  344. LOG.exception('Unexpected error:')
  345. return 2
  346. return 0
  347. if __name__ == "__main__":
  348. sys.exit(main())