yedit.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950
  1. #!/usr/bin/env python
  2. # pylint: disable=missing-docstring
  3. # ___ ___ _ _ ___ ___ _ _____ ___ ___
  4. # / __| __| \| | __| _ \ /_\_ _| __| \
  5. # | (_ | _|| .` | _|| / / _ \| | | _|| |) |
  6. # \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
  7. # | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
  8. # | |) | (_) | | .` | (_) || | | _|| |) | | | |
  9. # |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
  10. #
  11. # Copyright 2016 Red Hat, Inc. and/or its affiliates
  12. # and other contributors as indicated by the @author tags.
  13. #
  14. # Licensed under the Apache License, Version 2.0 (the "License");
  15. # you may not use this file except in compliance with the License.
  16. # You may obtain a copy of the License at
  17. #
  18. # http://www.apache.org/licenses/LICENSE-2.0
  19. #
  20. # Unless required by applicable law or agreed to in writing, software
  21. # distributed under the License is distributed on an "AS IS" BASIS,
  22. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  23. # See the License for the specific language governing permissions and
  24. # limitations under the License.
  25. #
  26. # -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
  27. # pylint: disable=wrong-import-order,wrong-import-position,unused-import
  28. from __future__ import print_function # noqa: F401
  29. import copy # noqa: F401
  30. import json # noqa: F401
  31. import os # noqa: F401
  32. import re # noqa: F401
  33. import shutil # noqa: F401
  34. import tempfile # noqa: F401
  35. import time # noqa: F401
  36. try:
  37. import ruamel.yaml as yaml # noqa: F401
  38. except ImportError:
  39. import yaml # noqa: F401
  40. from ansible.module_utils.basic import AnsibleModule
  41. # -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
  42. # -*- -*- -*- Begin included fragment: doc/yedit -*- -*- -*-
  43. DOCUMENTATION = '''
  44. ---
  45. module: yedit
  46. short_description: Create, modify, and idempotently manage yaml files.
  47. description:
  48. - Modify yaml files programmatically.
  49. options:
  50. state:
  51. description:
  52. - State represents whether to create, modify, delete, or list yaml
  53. required: true
  54. default: present
  55. choices: ["present", "absent", "list"]
  56. aliases: []
  57. debug:
  58. description:
  59. - Turn on debug information.
  60. required: false
  61. default: false
  62. aliases: []
  63. src:
  64. description:
  65. - The file that is the target of the modifications.
  66. required: false
  67. default: None
  68. aliases: []
  69. content:
  70. description:
  71. - Content represents the yaml content you desire to work with. This
  72. - could be the file contents to write or the inmemory data to modify.
  73. required: false
  74. default: None
  75. aliases: []
  76. content_type:
  77. description:
  78. - The python type of the content parameter.
  79. required: false
  80. default: 'dict'
  81. aliases: []
  82. key:
  83. description:
  84. - The path to the value you wish to modify. Emtpy string means the top of
  85. - the document.
  86. required: false
  87. default: ''
  88. aliases: []
  89. value:
  90. description:
  91. - The incoming value of parameter 'key'.
  92. required: false
  93. default:
  94. aliases: []
  95. value_type:
  96. description:
  97. - The python type of the incoming value.
  98. required: false
  99. default: ''
  100. aliases: []
  101. update:
  102. description:
  103. - Whether the update should be performed on a dict/hash or list/array
  104. - object.
  105. required: false
  106. default: false
  107. aliases: []
  108. append:
  109. description:
  110. - Whether to append to an array/list. When the key does not exist or is
  111. - null, a new array is created. When the key is of a non-list type,
  112. - nothing is done.
  113. required: false
  114. default: false
  115. aliases: []
  116. index:
  117. description:
  118. - Used in conjunction with the update parameter. This will update a
  119. - specific index in an array/list.
  120. required: false
  121. default: false
  122. aliases: []
  123. curr_value:
  124. description:
  125. - Used in conjunction with the update parameter. This is the current
  126. - value of 'key' in the yaml file.
  127. required: false
  128. default: false
  129. aliases: []
  130. curr_value_format:
  131. description:
  132. - Format of the incoming current value.
  133. choices: ["yaml", "json", "str"]
  134. required: false
  135. default: false
  136. aliases: []
  137. backup:
  138. description:
  139. - Whether to make a backup copy of the current file when performing an
  140. - edit.
  141. required: false
  142. default: true
  143. aliases: []
  144. separator:
  145. description:
  146. - The separator being used when parsing strings.
  147. required: false
  148. default: '.'
  149. aliases: []
  150. author:
  151. - "Kenny Woodson <kwoodson@redhat.com>"
  152. extends_documentation_fragment: []
  153. '''
  154. EXAMPLES = '''
  155. # Simple insert of key, value
  156. - name: insert simple key, value
  157. yedit:
  158. src: somefile.yml
  159. key: test
  160. value: somevalue
  161. state: present
  162. # Results:
  163. # test: somevalue
  164. # Multilevel insert of key, value
  165. - name: insert simple key, value
  166. yedit:
  167. src: somefile.yml
  168. key: a#b#c
  169. value: d
  170. state: present
  171. # Results:
  172. # a:
  173. # b:
  174. # c: d
  175. #
  176. # multiple edits at the same time
  177. - name: perform multiple edits
  178. yedit:
  179. src: somefile.yml
  180. edits:
  181. - key: a#b#c
  182. value: d
  183. - key: a#b#c#d
  184. value: e
  185. state: present
  186. # Results:
  187. # a:
  188. # b:
  189. # c:
  190. # d: e
  191. '''
  192. # -*- -*- -*- End included fragment: doc/yedit -*- -*- -*-
  193. # -*- -*- -*- Begin included fragment: class/yedit.py -*- -*- -*-
  194. class YeditException(Exception):
  195. ''' Exception class for Yedit '''
  196. pass
  197. # pylint: disable=too-many-public-methods
  198. class Yedit(object):
  199. ''' Class to modify yaml files '''
  200. re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
  201. re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
  202. com_sep = set(['.', '#', '|', ':'])
  203. # pylint: disable=too-many-arguments
  204. def __init__(self,
  205. filename=None,
  206. content=None,
  207. content_type='yaml',
  208. separator='.',
  209. backup=False):
  210. self.content = content
  211. self._separator = separator
  212. self.filename = filename
  213. self.__yaml_dict = content
  214. self.content_type = content_type
  215. self.backup = backup
  216. self.load(content_type=self.content_type)
  217. if self.__yaml_dict is None:
  218. self.__yaml_dict = {}
  219. @property
  220. def separator(self):
  221. ''' getter method for separator '''
  222. return self._separator
  223. @separator.setter
  224. def separator(self, inc_sep):
  225. ''' setter method for separator '''
  226. self._separator = inc_sep
  227. @property
  228. def yaml_dict(self):
  229. ''' getter method for yaml_dict '''
  230. return self.__yaml_dict
  231. @yaml_dict.setter
  232. def yaml_dict(self, value):
  233. ''' setter method for yaml_dict '''
  234. self.__yaml_dict = value
  235. @staticmethod
  236. def parse_key(key, sep='.'):
  237. '''parse the key allowing the appropriate separator'''
  238. common_separators = list(Yedit.com_sep - set([sep]))
  239. return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
  240. @staticmethod
  241. def valid_key(key, sep='.'):
  242. '''validate the incoming key'''
  243. common_separators = list(Yedit.com_sep - set([sep]))
  244. if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
  245. return False
  246. return True
  247. @staticmethod
  248. def remove_entry(data, key, sep='.'):
  249. ''' remove data at location key '''
  250. if key == '' and isinstance(data, dict):
  251. data.clear()
  252. return True
  253. elif key == '' and isinstance(data, list):
  254. del data[:]
  255. return True
  256. if not (key and Yedit.valid_key(key, sep)) and \
  257. isinstance(data, (list, dict)):
  258. return None
  259. key_indexes = Yedit.parse_key(key, sep)
  260. for arr_ind, dict_key in key_indexes[:-1]:
  261. if dict_key and isinstance(data, dict):
  262. data = data.get(dict_key)
  263. elif (arr_ind and isinstance(data, list) and
  264. int(arr_ind) <= len(data) - 1):
  265. data = data[int(arr_ind)]
  266. else:
  267. return None
  268. # process last index for remove
  269. # expected list entry
  270. if key_indexes[-1][0]:
  271. if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  272. del data[int(key_indexes[-1][0])]
  273. return True
  274. # expected dict entry
  275. elif key_indexes[-1][1]:
  276. if isinstance(data, dict):
  277. del data[key_indexes[-1][1]]
  278. return True
  279. @staticmethod
  280. def add_entry(data, key, item=None, sep='.'):
  281. ''' Get an item from a dictionary with key notation a.b.c
  282. d = {'a': {'b': 'c'}}}
  283. key = a#b
  284. return c
  285. '''
  286. if key == '':
  287. pass
  288. elif (not (key and Yedit.valid_key(key, sep)) and
  289. isinstance(data, (list, dict))):
  290. return None
  291. key_indexes = Yedit.parse_key(key, sep)
  292. for arr_ind, dict_key in key_indexes[:-1]:
  293. if dict_key:
  294. if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
  295. data = data[dict_key]
  296. continue
  297. elif data and not isinstance(data, dict):
  298. raise YeditException("Unexpected item type found while going through key " +
  299. "path: {} (at key: {})".format(key, dict_key))
  300. data[dict_key] = {}
  301. data = data[dict_key]
  302. elif (arr_ind and isinstance(data, list) and
  303. int(arr_ind) <= len(data) - 1):
  304. data = data[int(arr_ind)]
  305. else:
  306. raise YeditException("Unexpected item type found while going through key path: {}".format(key))
  307. if key == '':
  308. data = item
  309. # process last index for add
  310. # expected list entry
  311. elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  312. data[int(key_indexes[-1][0])] = item
  313. # expected dict entry
  314. elif key_indexes[-1][1] and isinstance(data, dict):
  315. data[key_indexes[-1][1]] = item
  316. # didn't add/update to an existing list, nor add/update key to a dict
  317. # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
  318. # non-existent array
  319. else:
  320. raise YeditException("Error adding to object at path: {}".format(key))
  321. return data
  322. @staticmethod
  323. def get_entry(data, key, sep='.'):
  324. ''' Get an item from a dictionary with key notation a.b.c
  325. d = {'a': {'b': 'c'}}}
  326. key = a.b
  327. return c
  328. '''
  329. if key == '':
  330. pass
  331. elif (not (key and Yedit.valid_key(key, sep)) and
  332. isinstance(data, (list, dict))):
  333. return None
  334. key_indexes = Yedit.parse_key(key, sep)
  335. for arr_ind, dict_key in key_indexes:
  336. if dict_key and isinstance(data, dict):
  337. data = data.get(dict_key)
  338. elif (arr_ind and isinstance(data, list) and
  339. int(arr_ind) <= len(data) - 1):
  340. data = data[int(arr_ind)]
  341. else:
  342. return None
  343. return data
  344. @staticmethod
  345. def _write(filename, contents):
  346. ''' Actually write the file contents to disk. This helps with mocking. '''
  347. tmp_filename = filename + '.yedit'
  348. with open(tmp_filename, 'w') as yfd:
  349. yfd.write(contents)
  350. os.rename(tmp_filename, filename)
  351. def write(self):
  352. ''' write to file '''
  353. if not self.filename:
  354. raise YeditException('Please specify a filename.')
  355. if self.backup and self.file_exists():
  356. shutil.copy(self.filename, self.filename + '.orig')
  357. # Try to set format attributes if supported
  358. try:
  359. self.yaml_dict.fa.set_block_style()
  360. except AttributeError:
  361. pass
  362. # Try to use RoundTripDumper if supported.
  363. if self.content_type == 'yaml':
  364. try:
  365. Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
  366. except AttributeError:
  367. Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
  368. elif self.content_type == 'json':
  369. Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
  370. else:
  371. raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
  372. 'Please specify a content_type of yaml or json.')
  373. return (True, self.yaml_dict)
  374. def read(self):
  375. ''' read from file '''
  376. # check if it exists
  377. if self.filename is None or not self.file_exists():
  378. return None
  379. contents = None
  380. with open(self.filename) as yfd:
  381. contents = yfd.read()
  382. return contents
  383. def file_exists(self):
  384. ''' return whether file exists '''
  385. if os.path.exists(self.filename):
  386. return True
  387. return False
  388. def load(self, content_type='yaml'):
  389. ''' return yaml file '''
  390. contents = self.read()
  391. if not contents and not self.content:
  392. return None
  393. if self.content:
  394. if isinstance(self.content, dict):
  395. self.yaml_dict = self.content
  396. return self.yaml_dict
  397. elif isinstance(self.content, str):
  398. contents = self.content
  399. # check if it is yaml
  400. try:
  401. if content_type == 'yaml' and contents:
  402. # Try to set format attributes if supported
  403. try:
  404. self.yaml_dict.fa.set_block_style()
  405. except AttributeError:
  406. pass
  407. # Try to use RoundTripLoader if supported.
  408. try:
  409. self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
  410. except AttributeError:
  411. self.yaml_dict = yaml.safe_load(contents)
  412. # Try to set format attributes if supported
  413. try:
  414. self.yaml_dict.fa.set_block_style()
  415. except AttributeError:
  416. pass
  417. elif content_type == 'json' and contents:
  418. self.yaml_dict = json.loads(contents)
  419. except yaml.YAMLError as err:
  420. # Error loading yaml or json
  421. raise YeditException('Problem with loading yaml file. {}'.format(err))
  422. return self.yaml_dict
  423. def get(self, key):
  424. ''' get a specified key'''
  425. try:
  426. entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
  427. except KeyError:
  428. entry = None
  429. return entry
  430. def pop(self, path, key_or_item):
  431. ''' remove a key, value pair from a dict or an item for a list'''
  432. try:
  433. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  434. except KeyError:
  435. entry = None
  436. if entry is None:
  437. return (False, self.yaml_dict)
  438. if isinstance(entry, dict):
  439. # AUDIT:maybe-no-member makes sense due to fuzzy types
  440. # pylint: disable=maybe-no-member
  441. if key_or_item in entry:
  442. entry.pop(key_or_item)
  443. return (True, self.yaml_dict)
  444. return (False, self.yaml_dict)
  445. elif isinstance(entry, list):
  446. # AUDIT:maybe-no-member makes sense due to fuzzy types
  447. # pylint: disable=maybe-no-member
  448. ind = None
  449. try:
  450. ind = entry.index(key_or_item)
  451. except ValueError:
  452. return (False, self.yaml_dict)
  453. entry.pop(ind)
  454. return (True, self.yaml_dict)
  455. return (False, self.yaml_dict)
  456. def delete(self, path):
  457. ''' remove path from a dict'''
  458. try:
  459. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  460. except KeyError:
  461. entry = None
  462. if entry is None:
  463. return (False, self.yaml_dict)
  464. result = Yedit.remove_entry(self.yaml_dict, path, self.separator)
  465. if not result:
  466. return (False, self.yaml_dict)
  467. return (True, self.yaml_dict)
  468. def exists(self, path, value):
  469. ''' check if value exists at path'''
  470. try:
  471. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  472. except KeyError:
  473. entry = None
  474. if isinstance(entry, list):
  475. if value in entry:
  476. return True
  477. return False
  478. elif isinstance(entry, dict):
  479. if isinstance(value, dict):
  480. rval = False
  481. for key, val in value.items():
  482. if entry[key] != val:
  483. rval = False
  484. break
  485. else:
  486. rval = True
  487. return rval
  488. return value in entry
  489. return entry == value
  490. def append(self, path, value):
  491. '''append value to a list'''
  492. try:
  493. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  494. except KeyError:
  495. entry = None
  496. if entry is None:
  497. self.put(path, [])
  498. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  499. if not isinstance(entry, list):
  500. return (False, self.yaml_dict)
  501. # AUDIT:maybe-no-member makes sense due to loading data from
  502. # a serialized format.
  503. # pylint: disable=maybe-no-member
  504. entry.append(value)
  505. return (True, self.yaml_dict)
  506. # pylint: disable=too-many-arguments
  507. def update(self, path, value, index=None, curr_value=None):
  508. ''' put path, value into a dict '''
  509. try:
  510. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  511. except KeyError:
  512. entry = None
  513. if isinstance(entry, dict):
  514. # AUDIT:maybe-no-member makes sense due to fuzzy types
  515. # pylint: disable=maybe-no-member
  516. if not isinstance(value, dict):
  517. raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
  518. 'value=[{}] type=[{}]'.format(value, type(value)))
  519. entry.update(value)
  520. return (True, self.yaml_dict)
  521. elif isinstance(entry, list):
  522. # AUDIT:maybe-no-member makes sense due to fuzzy types
  523. # pylint: disable=maybe-no-member
  524. ind = None
  525. if curr_value:
  526. try:
  527. ind = entry.index(curr_value)
  528. except ValueError:
  529. return (False, self.yaml_dict)
  530. elif index is not None:
  531. ind = index
  532. if ind is not None and entry[ind] != value:
  533. entry[ind] = value
  534. return (True, self.yaml_dict)
  535. # see if it exists in the list
  536. try:
  537. ind = entry.index(value)
  538. except ValueError:
  539. # doesn't exist, append it
  540. entry.append(value)
  541. return (True, self.yaml_dict)
  542. # already exists, return
  543. if ind is not None:
  544. return (False, self.yaml_dict)
  545. return (False, self.yaml_dict)
  546. def put(self, path, value):
  547. ''' put path, value into a dict '''
  548. try:
  549. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  550. except KeyError:
  551. entry = None
  552. if entry == value:
  553. return (False, self.yaml_dict)
  554. # deepcopy didn't work
  555. # Try to use ruamel.yaml and fallback to pyyaml
  556. try:
  557. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  558. default_flow_style=False),
  559. yaml.RoundTripLoader)
  560. except AttributeError:
  561. tmp_copy = copy.deepcopy(self.yaml_dict)
  562. # set the format attributes if available
  563. try:
  564. tmp_copy.fa.set_block_style()
  565. except AttributeError:
  566. pass
  567. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  568. if result is None:
  569. return (False, self.yaml_dict)
  570. # When path equals "" it is a special case.
  571. # "" refers to the root of the document
  572. # Only update the root path (entire document) when its a list or dict
  573. if path == '':
  574. if isinstance(result, list) or isinstance(result, dict):
  575. self.yaml_dict = result
  576. return (True, self.yaml_dict)
  577. return (False, self.yaml_dict)
  578. self.yaml_dict = tmp_copy
  579. return (True, self.yaml_dict)
  580. def create(self, path, value):
  581. ''' create a yaml file '''
  582. if not self.file_exists():
  583. # deepcopy didn't work
  584. # Try to use ruamel.yaml and fallback to pyyaml
  585. try:
  586. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  587. default_flow_style=False),
  588. yaml.RoundTripLoader)
  589. except AttributeError:
  590. tmp_copy = copy.deepcopy(self.yaml_dict)
  591. # set the format attributes if available
  592. try:
  593. tmp_copy.fa.set_block_style()
  594. except AttributeError:
  595. pass
  596. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  597. if result is not None:
  598. self.yaml_dict = tmp_copy
  599. return (True, self.yaml_dict)
  600. return (False, self.yaml_dict)
  601. @staticmethod
  602. def get_curr_value(invalue, val_type):
  603. '''return the current value'''
  604. if invalue is None:
  605. return None
  606. curr_value = invalue
  607. if val_type == 'yaml':
  608. try:
  609. # AUDIT:maybe-no-member makes sense due to different yaml libraries
  610. # pylint: disable=maybe-no-member
  611. curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
  612. except AttributeError:
  613. curr_value = yaml.safe_load(invalue)
  614. elif val_type == 'json':
  615. curr_value = json.loads(invalue)
  616. return curr_value
  617. @staticmethod
  618. def parse_value(inc_value, vtype=''):
  619. '''determine value type passed'''
  620. true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
  621. 'on', 'On', 'ON', ]
  622. false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
  623. 'off', 'Off', 'OFF']
  624. # It came in as a string but you didn't specify value_type as string
  625. # we will convert to bool if it matches any of the above cases
  626. if isinstance(inc_value, str) and 'bool' in vtype:
  627. if inc_value not in true_bools and inc_value not in false_bools:
  628. raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
  629. elif isinstance(inc_value, bool) and 'str' in vtype:
  630. inc_value = str(inc_value)
  631. # There is a special case where '' will turn into None after yaml loading it so skip
  632. if isinstance(inc_value, str) and inc_value == '':
  633. pass
  634. # If vtype is not str then go ahead and attempt to yaml load it.
  635. elif isinstance(inc_value, str) and 'str' not in vtype:
  636. try:
  637. inc_value = yaml.safe_load(inc_value)
  638. except Exception:
  639. raise YeditException('Could not determine type of incoming value. ' +
  640. 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
  641. return inc_value
  642. @staticmethod
  643. def process_edits(edits, yamlfile):
  644. '''run through a list of edits and process them one-by-one'''
  645. results = []
  646. for edit in edits:
  647. value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
  648. if edit.get('action') == 'update':
  649. # pylint: disable=line-too-long
  650. curr_value = Yedit.get_curr_value(
  651. Yedit.parse_value(edit.get('curr_value')),
  652. edit.get('curr_value_format'))
  653. rval = yamlfile.update(edit['key'],
  654. value,
  655. edit.get('index'),
  656. curr_value)
  657. elif edit.get('action') == 'append':
  658. rval = yamlfile.append(edit['key'], value)
  659. else:
  660. rval = yamlfile.put(edit['key'], value)
  661. if rval[0]:
  662. results.append({'key': edit['key'], 'edit': rval[1]})
  663. return {'changed': len(results) > 0, 'results': results}
  664. # pylint: disable=too-many-return-statements,too-many-branches
  665. @staticmethod
  666. def run_ansible(params):
  667. '''perform the idempotent crud operations'''
  668. yamlfile = Yedit(filename=params['src'],
  669. backup=params['backup'],
  670. content_type=params['content_type'],
  671. separator=params['separator'])
  672. state = params['state']
  673. if params['src']:
  674. rval = yamlfile.load()
  675. if yamlfile.yaml_dict is None and state != 'present':
  676. return {'failed': True,
  677. 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
  678. 'file exists, that it is has correct permissions, and is valid yaml.'}
  679. if state == 'list':
  680. if params['content']:
  681. content = Yedit.parse_value(params['content'], params['content_type'])
  682. yamlfile.yaml_dict = content
  683. if params['key']:
  684. rval = yamlfile.get(params['key'])
  685. return {'changed': False, 'result': rval, 'state': state}
  686. elif state == 'absent':
  687. if params['content']:
  688. content = Yedit.parse_value(params['content'], params['content_type'])
  689. yamlfile.yaml_dict = content
  690. if params['update']:
  691. rval = yamlfile.pop(params['key'], params['value'])
  692. else:
  693. rval = yamlfile.delete(params['key'])
  694. if rval[0] and params['src']:
  695. yamlfile.write()
  696. return {'changed': rval[0], 'result': rval[1], 'state': state}
  697. elif state == 'present':
  698. # check if content is different than what is in the file
  699. if params['content']:
  700. content = Yedit.parse_value(params['content'], params['content_type'])
  701. # We had no edits to make and the contents are the same
  702. if yamlfile.yaml_dict == content and \
  703. params['value'] is None:
  704. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  705. yamlfile.yaml_dict = content
  706. # If we were passed a key, value then
  707. # we enapsulate it in a list and process it
  708. # Key, Value passed to the module : Converted to Edits list #
  709. edits = []
  710. _edit = {}
  711. if params['value'] is not None:
  712. _edit['value'] = params['value']
  713. _edit['value_type'] = params['value_type']
  714. _edit['key'] = params['key']
  715. if params['update']:
  716. _edit['action'] = 'update'
  717. _edit['curr_value'] = params['curr_value']
  718. _edit['curr_value_format'] = params['curr_value_format']
  719. _edit['index'] = params['index']
  720. elif params['append']:
  721. _edit['action'] = 'append'
  722. edits.append(_edit)
  723. elif params['edits'] is not None:
  724. edits = params['edits']
  725. if edits:
  726. results = Yedit.process_edits(edits, yamlfile)
  727. # if there were changes and a src provided to us we need to write
  728. if results['changed'] and params['src']:
  729. yamlfile.write()
  730. return {'changed': results['changed'], 'result': results['results'], 'state': state}
  731. # no edits to make
  732. if params['src']:
  733. # pylint: disable=redefined-variable-type
  734. rval = yamlfile.write()
  735. return {'changed': rval[0],
  736. 'result': rval[1],
  737. 'state': state}
  738. # We were passed content but no src, key or value, or edits. Return contents in memory
  739. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  740. return {'failed': True, 'msg': 'Unkown state passed'}
  741. # -*- -*- -*- End included fragment: class/yedit.py -*- -*- -*-
  742. # -*- -*- -*- Begin included fragment: ansible/yedit.py -*- -*- -*-
  743. # pylint: disable=too-many-branches
  744. def main():
  745. ''' ansible oc module for secrets '''
  746. module = AnsibleModule(
  747. argument_spec=dict(
  748. state=dict(default='present', type='str',
  749. choices=['present', 'absent', 'list']),
  750. debug=dict(default=False, type='bool'),
  751. src=dict(default=None, type='str'),
  752. content=dict(default=None),
  753. content_type=dict(default='yaml', choices=['yaml', 'json']),
  754. key=dict(default='', type='str'),
  755. value=dict(),
  756. value_type=dict(default='', type='str'),
  757. update=dict(default=False, type='bool'),
  758. append=dict(default=False, type='bool'),
  759. index=dict(default=None, type='int'),
  760. curr_value=dict(default=None, type='str'),
  761. curr_value_format=dict(default='yaml',
  762. choices=['yaml', 'json', 'str'],
  763. type='str'),
  764. backup=dict(default=True, type='bool'),
  765. separator=dict(default='.', type='str'),
  766. edits=dict(default=None, type='list'),
  767. ),
  768. mutually_exclusive=[["curr_value", "index"], ['update', "append"]],
  769. required_one_of=[["content", "src"]],
  770. )
  771. # Verify we recieved either a valid key or edits with valid keys when receiving a src file.
  772. # A valid key being not None or not ''.
  773. if module.params['src'] is not None:
  774. key_error = False
  775. edit_error = False
  776. if module.params['key'] in [None, '']:
  777. key_error = True
  778. if module.params['edits'] in [None, []]:
  779. edit_error = True
  780. else:
  781. for edit in module.params['edits']:
  782. if edit.get('key') in [None, '']:
  783. edit_error = True
  784. break
  785. if key_error and edit_error:
  786. module.fail_json(failed=True, msg='Empty value for parameter key not allowed.')
  787. rval = Yedit.run_ansible(module.params)
  788. if 'failed' in rval and rval['failed']:
  789. module.fail_json(**rval)
  790. module.exit_json(**rval)
  791. if __name__ == '__main__':
  792. main()
  793. # -*- -*- -*- End included fragment: ansible/yedit.py -*- -*- -*-