yedit.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. #!/usr/bin/env python
  2. # ___ ___ _ _ ___ ___ _ _____ ___ ___
  3. # / __| __| \| | __| _ \ /_\_ _| __| \
  4. # | (_ | _|| .` | _|| / / _ \| | | _|| |) |
  5. # \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
  6. # | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
  7. # | |) | (_) | | .` | (_) || | | _|| |) | | | |
  8. # |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
  9. '''
  10. module for managing yaml files
  11. '''
  12. import os
  13. import re
  14. import yaml
  15. # This is here because of a bug that causes yaml
  16. # to incorrectly handle timezone info on timestamps
  17. def timestamp_constructor(_, node):
  18. ''' return timestamps as strings'''
  19. return str(node.value)
  20. yaml.add_constructor(u'tag:yaml.org,2002:timestamp', timestamp_constructor)
  21. class YeditException(Exception):
  22. ''' Exception class for Yedit '''
  23. pass
  24. class Yedit(object):
  25. ''' Class to modify yaml files '''
  26. re_valid_key = r"(((\[-?\d+\])|(\w+)).?)+$"
  27. re_key = r"(?:\[(-?\d+)\])|(\w+)"
  28. def __init__(self, filename=None, content=None, content_type='yaml'):
  29. self.content = content
  30. self.filename = filename
  31. self.__yaml_dict = content
  32. self.content_type = content_type
  33. if self.filename and not self.content:
  34. self.load(content_type=self.content_type)
  35. @property
  36. def yaml_dict(self):
  37. ''' getter method for yaml_dict '''
  38. return self.__yaml_dict
  39. @yaml_dict.setter
  40. def yaml_dict(self, value):
  41. ''' setter method for yaml_dict '''
  42. self.__yaml_dict = value
  43. @staticmethod
  44. def remove_entry(data, key):
  45. ''' remove data at location key '''
  46. if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))):
  47. return None
  48. key_indexes = re.findall(Yedit.re_key, key)
  49. for arr_ind, dict_key in key_indexes[:-1]:
  50. if dict_key and isinstance(data, dict):
  51. data = data.get(dict_key, None)
  52. elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1:
  53. data = data[int(arr_ind)]
  54. else:
  55. return None
  56. # process last index for remove
  57. # expected list entry
  58. if key_indexes[-1][0]:
  59. if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1:
  60. del data[int(key_indexes[-1][0])]
  61. # expected dict entry
  62. elif key_indexes[-1][1]:
  63. if isinstance(data, dict):
  64. del data[key_indexes[-1][1]]
  65. @staticmethod
  66. def add_entry(data, key, item=None):
  67. ''' Get an item from a dictionary with key notation a.b.c
  68. d = {'a': {'b': 'c'}}}
  69. key = a.b
  70. return c
  71. '''
  72. if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))):
  73. return None
  74. curr_data = data
  75. key_indexes = re.findall(Yedit.re_key, key)
  76. for arr_ind, dict_key in key_indexes[:-1]:
  77. if dict_key:
  78. if isinstance(data, dict) and data.has_key(dict_key):
  79. data = data[dict_key]
  80. continue
  81. data[dict_key] = {}
  82. data = data[dict_key]
  83. elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1:
  84. data = data[int(arr_ind)]
  85. else:
  86. return None
  87. # process last index for add
  88. # expected list entry
  89. if key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1:
  90. data[int(key_indexes[-1][0])] = item
  91. # expected dict entry
  92. elif key_indexes[-1][1] and isinstance(data, dict):
  93. data[key_indexes[-1][1]] = item
  94. return curr_data
  95. @staticmethod
  96. def get_entry(data, key):
  97. ''' Get an item from a dictionary with key notation a.b.c
  98. d = {'a': {'b': 'c'}}}
  99. key = a.b
  100. return c
  101. '''
  102. if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))):
  103. return None
  104. key_indexes = re.findall(Yedit.re_key, key)
  105. for arr_ind, dict_key in key_indexes:
  106. if dict_key and isinstance(data, dict):
  107. data = data.get(dict_key, None)
  108. elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1:
  109. data = data[int(arr_ind)]
  110. else:
  111. return None
  112. return data
  113. def write(self):
  114. ''' write to file '''
  115. if not self.filename:
  116. raise YeditException('Please specify a filename.')
  117. with open(self.filename, 'w') as yfd:
  118. yfd.write(yaml.safe_dump(self.yaml_dict, default_flow_style=False))
  119. def read(self):
  120. ''' write to file '''
  121. # check if it exists
  122. if not self.exists():
  123. return None
  124. contents = None
  125. with open(self.filename) as yfd:
  126. contents = yfd.read()
  127. return contents
  128. def exists(self):
  129. ''' return whether file exists '''
  130. if os.path.exists(self.filename):
  131. return True
  132. return False
  133. def load(self, content_type='yaml'):
  134. ''' return yaml file '''
  135. contents = self.read()
  136. if not contents:
  137. return None
  138. # check if it is yaml
  139. try:
  140. if content_type == 'yaml':
  141. self.yaml_dict = yaml.load(contents)
  142. elif content_type == 'json':
  143. self.yaml_dict = json.loads(contents)
  144. except yaml.YAMLError as _:
  145. # Error loading yaml or json
  146. return None
  147. return self.yaml_dict
  148. def get(self, key):
  149. ''' get a specified key'''
  150. try:
  151. entry = Yedit.get_entry(self.yaml_dict, key)
  152. except KeyError as _:
  153. entry = None
  154. return entry
  155. def delete(self, key):
  156. ''' put key, value into a yaml file '''
  157. try:
  158. entry = Yedit.get_entry(self.yaml_dict, key)
  159. except KeyError as _:
  160. entry = None
  161. if not entry:
  162. return (False, self.yaml_dict)
  163. Yedit.remove_entry(self.yaml_dict, key)
  164. return (True, self.yaml_dict)
  165. def put(self, key, value):
  166. ''' put key, value into a yaml file '''
  167. try:
  168. entry = Yedit.get_entry(self.yaml_dict, key)
  169. except KeyError as _:
  170. entry = None
  171. if entry == value:
  172. return (False, self.yaml_dict)
  173. Yedit.add_entry(self.yaml_dict, key, value)
  174. return (True, self.yaml_dict)
  175. def create(self, key, value):
  176. ''' create the file '''
  177. if not self.exists():
  178. self.yaml_dict = {key: value}
  179. return (True, self.yaml_dict)
  180. return (False, self.yaml_dict)
  181. def main():
  182. '''
  183. ansible oc module for secrets
  184. '''
  185. module = AnsibleModule(
  186. argument_spec=dict(
  187. state=dict(default='present', type='str',
  188. choices=['present', 'absent', 'list']),
  189. debug=dict(default=False, type='bool'),
  190. src=dict(default=None, type='str'),
  191. content=dict(default=None, type='dict'),
  192. key=dict(default=None, type='str'),
  193. value=dict(default=None, type='str'),
  194. value_format=dict(default='yaml', choices=['yaml', 'json'], type='str'),
  195. ),
  196. #mutually_exclusive=[["src", "content"]],
  197. supports_check_mode=True,
  198. )
  199. state = module.params['state']
  200. yamlfile = Yedit(module.params['src'], module.params['content'])
  201. rval = yamlfile.load()
  202. if not rval and state != 'present':
  203. module.fail_json(msg='Error opening file [%s]. Verify that the' + \
  204. ' file exists, that it is has correct permissions, and is valid yaml.')
  205. if state == 'list':
  206. module.exit_json(changed=False, results=rval, state="list")
  207. if state == 'absent':
  208. rval = yamlfile.delete(module.params['key'])
  209. module.exit_json(changed=rval[0], results=rval[1], state="absent")
  210. if state == 'present':
  211. if module.params['value_format'] == 'yaml':
  212. value = yaml.load(module.params['value'])
  213. elif module.params['value_format'] == 'json':
  214. value = json.loads(module.params['value'])
  215. if rval:
  216. rval = yamlfile.put(module.params['key'], value)
  217. module.exit_json(changed=rval[0], results=rval[1], state="present")
  218. if not module.params['content']:
  219. rval = yamlfile.create(module.params['key'], value)
  220. else:
  221. yamlfile.write()
  222. rval = yamlfile.load()
  223. module.exit_json(changed=rval[0], results=rval[1], state="present")
  224. module.exit_json(failed=True,
  225. changed=False,
  226. results='Unknown state passed. %s' % state,
  227. state="unknown")
  228. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, locally-disabled
  229. # import module snippets. This are required
  230. from ansible.module_utils.basic import *
  231. main()