yedit.py 30 KB

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