master_env_config_migrate.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. #!/usr/bin/env python
  2. # pylint: disable=missing-docstring
  3. #
  4. # Copyright 2018 Red Hat, Inc. and/or its affiliates
  5. # and other contributors as indicated by the @author tags.
  6. #
  7. # Licensed under the Apache License, Version 2.0 (the "License");
  8. # you may not use this file except in compliance with the License.
  9. # You may obtain a copy of the License at
  10. #
  11. # http://www.apache.org/licenses/LICENSE-2.0
  12. #
  13. # Unless required by applicable law or agreed to in writing, software
  14. # distributed under the License is distributed on an "AS IS" BASIS,
  15. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. # See the License for the specific language governing permissions and
  17. # limitations under the License.
  18. try:
  19. import configparser
  20. CONFIG_PROXY_NEW = True
  21. except ImportError:
  22. # configparser is available in python 2.7 backports, but that package
  23. # might not be installed.
  24. import ConfigParser as configparser
  25. CONFIG_PROXY_NEW = False
  26. import sys
  27. import os
  28. from ansible.module_utils.basic import AnsibleModule
  29. DOCUMENTATION = '''
  30. ---
  31. module: master_env_config_migrate
  32. short_description: Migrates an environment file from one location to another.
  33. version_added: "2.4"
  34. description:
  35. - Ensures that an environment file is properly migrated and values are properly
  36. quoted.
  37. options:
  38. src:
  39. description:
  40. - This is the original file on remote host.
  41. required: true
  42. dest:
  43. description:
  44. - This is the output location.
  45. required: true
  46. author:
  47. - "Michael Gugino <mgugino@redhat.com>"
  48. '''
  49. class SectionlessParser(configparser.RawConfigParser):
  50. # pylint: disable=invalid-name,too-many-locals,too-many-branches,too-many-statements
  51. # pylint: disable=anomalous-backslash-in-string,raising-bad-type
  52. """RawConfigParser that allows no sections"""
  53. # This code originally retrieved from:
  54. # https://github.com/python/cpython/blob/master/Lib/configparser.py
  55. # Copyright 2001-2018 Python Software Foundation; All Rights Reserved
  56. # Modified to allow no sections.
  57. def optionxform(self, optionstr):
  58. """Override this method, don't set .lower()"""
  59. return optionstr
  60. def _write_section(self, fp, section_name, section_items, delimiter):
  61. """Override for formatting"""
  62. for key, value in section_items:
  63. if " " in value and "\ " not in value and not value.startswith('"'):
  64. value = u'"{}"'.format(value)
  65. value = self._interpolation.before_write(self, section_name, key,
  66. value)
  67. if value is not None or not self._allow_no_value:
  68. value = delimiter + str(value).replace('\n', '\n\t')
  69. else:
  70. value = u""
  71. fp.write(u"{}{}\n".format(key, value))
  72. fp.write(u"\n")
  73. def write(self, fp, space_around_delimiters=True):
  74. """Ovrride write method"""
  75. delimiters = ('=', ':')
  76. if space_around_delimiters:
  77. d = " {} ".format(delimiters[0])
  78. else:
  79. d = delimiters[0]
  80. for section in self._sections:
  81. self._write_section(fp, section,
  82. self._sections[section].items(), d)
  83. def _set_proxies(self, sectname):
  84. """set proxies"""
  85. self._proxies[sectname] = configparser.SectionProxy(self, sectname)
  86. def _read(self, fp, fpname):
  87. """Parse a sectionless configuration file."""
  88. elements_added = set()
  89. cursect = {}
  90. sectname = '__none_sect'
  91. self._sections[sectname] = cursect
  92. self._set_proxies(sectname)
  93. optname = None
  94. lineno = 0
  95. indent_level = 0
  96. e = None # None, or an exception
  97. for lineno, line in enumerate(fp, start=1):
  98. comment_start = sys.maxsize
  99. # strip inline comments
  100. inline_prefixes = {p: -1 for p in self._inline_comment_prefixes}
  101. while comment_start == sys.maxsize and inline_prefixes:
  102. next_prefixes = {}
  103. for prefix, index in inline_prefixes.items():
  104. index = line.find(prefix, index + 1)
  105. if index == -1:
  106. continue
  107. next_prefixes[prefix] = index
  108. if index == 0 or (index > 0 and line[index - 1].isspace()):
  109. comment_start = min(comment_start, index)
  110. inline_prefixes = next_prefixes
  111. # strip full line comments
  112. for prefix in self._comment_prefixes:
  113. if line.strip().startswith(prefix):
  114. comment_start = 0
  115. break
  116. if comment_start == sys.maxsize:
  117. comment_start = None
  118. value = line[:comment_start].strip()
  119. if not value:
  120. if self._empty_lines_in_values:
  121. # add empty line to the value, but only if there was no
  122. # comment on the line
  123. if (comment_start is None and
  124. cursect is not None and
  125. optname and
  126. cursect[optname] is not None):
  127. cursect[optname].append('') # newlines added at join
  128. else:
  129. # empty line marks end of value
  130. indent_level = sys.maxsize
  131. continue
  132. # continuation line?
  133. first_nonspace = self.NONSPACECRE.search(line)
  134. cur_indent_level = first_nonspace.start() if first_nonspace else 0
  135. if (cursect is not None and optname and
  136. cur_indent_level > indent_level):
  137. cursect[optname].append(value)
  138. # a section header or option header?
  139. else:
  140. indent_level = cur_indent_level
  141. # is it a section header?
  142. mo = self.SECTCRE.match(value)
  143. if mo:
  144. optname = None
  145. else:
  146. mo = self._optcre.match(value)
  147. if mo:
  148. optname, _, optval = mo.group('option', 'vi', 'value')
  149. if not optname:
  150. e = self._handle_error(e, fpname, lineno, line)
  151. optname = self.optionxform(optname.rstrip())
  152. elements_added.add((sectname, optname))
  153. if optval is not None:
  154. optval = optval.strip()
  155. cursect[optname] = [optval]
  156. else:
  157. # valueless option handling
  158. cursect[optname] = None
  159. else:
  160. e = self._handle_error(e, fpname, lineno, line)
  161. self._join_multiline_values()
  162. # if any parsing errors occurred, raise an exception
  163. if e:
  164. raise e
  165. # pylint: disable=R0901,C0103,R0204
  166. class SectionlessParserOld(SectionlessParser):
  167. """Overrides write method to utilize newer abstraction"""
  168. def _set_proxies(self, sectname):
  169. """proxies not present in old version"""
  170. pass
  171. def create_file(src, dest):
  172. '''Create the dest file from src file'''
  173. if CONFIG_PROXY_NEW:
  174. config = SectionlessParser()
  175. else:
  176. config = SectionlessParserOld()
  177. config.readfp(open(src))
  178. with open(dest, 'w') as output:
  179. config.write(output, False)
  180. def run_module():
  181. '''Run this module'''
  182. module_args = dict(
  183. src=dict(required=True, type='path'),
  184. dest=dict(required=True, type='path'),
  185. )
  186. module = AnsibleModule(
  187. argument_spec=module_args,
  188. supports_check_mode=False
  189. )
  190. # First, create our dest dir if necessary
  191. dest = module.params['dest']
  192. src = module.params['src']
  193. if os.path.exists(dest):
  194. # Do nothing, output file already in place.
  195. result = {'changed': False}
  196. module.exit_json(**result)
  197. create_file(src, dest)
  198. result = {'changed': True}
  199. module.exit_json(**result)
  200. def main():
  201. run_module()
  202. if __name__ == '__main__':
  203. main()