oc_clusterrole.py 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868
  1. #!/usr/bin/env python
  2. # pylint: disable=missing-docstring
  3. # flake8: noqa: T001
  4. # ___ ___ _ _ ___ ___ _ _____ ___ ___
  5. # / __| __| \| | __| _ \ /_\_ _| __| \
  6. # | (_ | _|| .` | _|| / / _ \| | | _|| |) |
  7. # \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
  8. # | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
  9. # | |) | (_) | | .` | (_) || | | _|| |) | | | |
  10. # |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
  11. #
  12. # Copyright 2016 Red Hat, Inc. and/or its affiliates
  13. # and other contributors as indicated by the @author tags.
  14. #
  15. # Licensed under the Apache License, Version 2.0 (the "License");
  16. # you may not use this file except in compliance with the License.
  17. # You may obtain a copy of the License at
  18. #
  19. # http://www.apache.org/licenses/LICENSE-2.0
  20. #
  21. # Unless required by applicable law or agreed to in writing, software
  22. # distributed under the License is distributed on an "AS IS" BASIS,
  23. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  24. # See the License for the specific language governing permissions and
  25. # limitations under the License.
  26. #
  27. # -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
  28. '''
  29. OpenShiftCLI class that wraps the oc commands in a subprocess
  30. '''
  31. # pylint: disable=too-many-lines
  32. from __future__ import print_function
  33. import atexit
  34. import copy
  35. import fcntl
  36. import json
  37. import time
  38. import os
  39. import re
  40. import shutil
  41. import subprocess
  42. import tempfile
  43. # pylint: disable=import-error
  44. try:
  45. import ruamel.yaml as yaml
  46. except ImportError:
  47. import yaml
  48. from ansible.module_utils.basic import AnsibleModule
  49. # -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
  50. # -*- -*- -*- Begin included fragment: doc/clusterrole -*- -*- -*-
  51. DOCUMENTATION = '''
  52. ---
  53. module: oc_clusterrole
  54. short_description: Modify, and idempotently manage openshift clusterroles
  55. description:
  56. - Manage openshift clusterroles
  57. options:
  58. state:
  59. description:
  60. - Supported states, present, absent, list
  61. - present - will ensure object is created or updated to the value specified
  62. - list - will return a clusterrole
  63. - absent - will remove a clusterrole
  64. required: False
  65. default: present
  66. choices: ["present", 'absent', 'list']
  67. aliases: []
  68. kubeconfig:
  69. description:
  70. - The path for the kubeconfig file to use for authentication
  71. required: false
  72. default: /etc/origin/master/admin.kubeconfig
  73. aliases: []
  74. debug:
  75. description:
  76. - Turn on debug output.
  77. required: false
  78. default: False
  79. aliases: []
  80. name:
  81. description:
  82. - Name of the object that is being queried.
  83. required: false
  84. default: None
  85. aliases: []
  86. rules:
  87. description:
  88. - A list of dictionaries that have the rule parameters.
  89. - e.g. rules=[{'apiGroups': [""], 'attributeRestrictions': None, 'verbs': ['get'], 'resources': []}]
  90. required: false
  91. default: None
  92. aliases: []
  93. author:
  94. - "Kenny Woodson <kwoodson@redhat.com>"
  95. extends_documentation_fragment: []
  96. '''
  97. EXAMPLES = '''
  98. - name: query a list of env vars on dc
  99. oc_clusterrole:
  100. name: myclusterrole
  101. state: list
  102. - name: Set the following variables.
  103. oc_clusterrole:
  104. name: myclusterrole
  105. rules:
  106. apiGroups:
  107. - ""
  108. attributeRestrictions: null
  109. verbs: []
  110. resources: []
  111. '''
  112. # -*- -*- -*- End included fragment: doc/clusterrole -*- -*- -*-
  113. # -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
  114. class YeditException(Exception): # pragma: no cover
  115. ''' Exception class for Yedit '''
  116. pass
  117. # pylint: disable=too-many-public-methods
  118. class Yedit(object): # pragma: no cover
  119. ''' Class to modify yaml files '''
  120. re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
  121. re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
  122. com_sep = set(['.', '#', '|', ':'])
  123. # pylint: disable=too-many-arguments
  124. def __init__(self,
  125. filename=None,
  126. content=None,
  127. content_type='yaml',
  128. separator='.',
  129. backup=False):
  130. self.content = content
  131. self._separator = separator
  132. self.filename = filename
  133. self.__yaml_dict = content
  134. self.content_type = content_type
  135. self.backup = backup
  136. self.load(content_type=self.content_type)
  137. if self.__yaml_dict is None:
  138. self.__yaml_dict = {}
  139. @property
  140. def separator(self):
  141. ''' getter method for separator '''
  142. return self._separator
  143. @separator.setter
  144. def separator(self, inc_sep):
  145. ''' setter method for separator '''
  146. self._separator = inc_sep
  147. @property
  148. def yaml_dict(self):
  149. ''' getter method for yaml_dict '''
  150. return self.__yaml_dict
  151. @yaml_dict.setter
  152. def yaml_dict(self, value):
  153. ''' setter method for yaml_dict '''
  154. self.__yaml_dict = value
  155. @staticmethod
  156. def parse_key(key, sep='.'):
  157. '''parse the key allowing the appropriate separator'''
  158. common_separators = list(Yedit.com_sep - set([sep]))
  159. return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
  160. @staticmethod
  161. def valid_key(key, sep='.'):
  162. '''validate the incoming key'''
  163. common_separators = list(Yedit.com_sep - set([sep]))
  164. if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
  165. return False
  166. return True
  167. # pylint: disable=too-many-return-statements,too-many-branches
  168. @staticmethod
  169. def remove_entry(data, key, index=None, value=None, sep='.'):
  170. ''' remove data at location key '''
  171. if key == '' and isinstance(data, dict):
  172. if value is not None:
  173. data.pop(value)
  174. elif index is not None:
  175. raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
  176. else:
  177. data.clear()
  178. return True
  179. elif key == '' and isinstance(data, list):
  180. ind = None
  181. if value is not None:
  182. try:
  183. ind = data.index(value)
  184. except ValueError:
  185. return False
  186. elif index is not None:
  187. ind = index
  188. else:
  189. del data[:]
  190. if ind is not None:
  191. data.pop(ind)
  192. return True
  193. if not (key and Yedit.valid_key(key, sep)) and \
  194. isinstance(data, (list, dict)):
  195. return None
  196. key_indexes = Yedit.parse_key(key, sep)
  197. for arr_ind, dict_key in key_indexes[:-1]:
  198. if dict_key and isinstance(data, dict):
  199. data = data.get(dict_key)
  200. elif (arr_ind and isinstance(data, list) and
  201. int(arr_ind) <= len(data) - 1):
  202. data = data[int(arr_ind)]
  203. else:
  204. return None
  205. # process last index for remove
  206. # expected list entry
  207. if key_indexes[-1][0]:
  208. if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  209. del data[int(key_indexes[-1][0])]
  210. return True
  211. # expected dict entry
  212. elif key_indexes[-1][1]:
  213. if isinstance(data, dict):
  214. del data[key_indexes[-1][1]]
  215. return True
  216. @staticmethod
  217. def add_entry(data, key, item=None, sep='.'):
  218. ''' Get an item from a dictionary with key notation a.b.c
  219. d = {'a': {'b': 'c'}}}
  220. key = a#b
  221. return c
  222. '''
  223. if key == '':
  224. pass
  225. elif (not (key and Yedit.valid_key(key, sep)) and
  226. isinstance(data, (list, dict))):
  227. return None
  228. key_indexes = Yedit.parse_key(key, sep)
  229. for arr_ind, dict_key in key_indexes[:-1]:
  230. if dict_key:
  231. if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
  232. data = data[dict_key]
  233. continue
  234. elif data and not isinstance(data, dict):
  235. raise YeditException("Unexpected item type found while going through key " +
  236. "path: {} (at key: {})".format(key, dict_key))
  237. data[dict_key] = {}
  238. data = data[dict_key]
  239. elif (arr_ind and isinstance(data, list) and
  240. int(arr_ind) <= len(data) - 1):
  241. data = data[int(arr_ind)]
  242. else:
  243. raise YeditException("Unexpected item type found while going through key path: {}".format(key))
  244. if key == '':
  245. data = item
  246. # process last index for add
  247. # expected list entry
  248. elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  249. data[int(key_indexes[-1][0])] = item
  250. # expected dict entry
  251. elif key_indexes[-1][1] and isinstance(data, dict):
  252. data[key_indexes[-1][1]] = item
  253. # didn't add/update to an existing list, nor add/update key to a dict
  254. # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
  255. # non-existent array
  256. else:
  257. raise YeditException("Error adding to object at path: {}".format(key))
  258. return data
  259. @staticmethod
  260. def get_entry(data, key, sep='.'):
  261. ''' Get an item from a dictionary with key notation a.b.c
  262. d = {'a': {'b': 'c'}}}
  263. key = a.b
  264. return c
  265. '''
  266. if key == '':
  267. pass
  268. elif (not (key and Yedit.valid_key(key, sep)) and
  269. isinstance(data, (list, dict))):
  270. return None
  271. key_indexes = Yedit.parse_key(key, sep)
  272. for arr_ind, dict_key in key_indexes:
  273. if dict_key and isinstance(data, dict):
  274. data = data.get(dict_key)
  275. elif (arr_ind and isinstance(data, list) and
  276. int(arr_ind) <= len(data) - 1):
  277. data = data[int(arr_ind)]
  278. else:
  279. return None
  280. return data
  281. @staticmethod
  282. def _write(filename, contents):
  283. ''' Actually write the file contents to disk. This helps with mocking. '''
  284. tmp_filename = filename + '.yedit'
  285. with open(tmp_filename, 'w') as yfd:
  286. fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  287. yfd.write(contents)
  288. fcntl.flock(yfd, fcntl.LOCK_UN)
  289. os.rename(tmp_filename, filename)
  290. def write(self):
  291. ''' write to file '''
  292. if not self.filename:
  293. raise YeditException('Please specify a filename.')
  294. if self.backup and self.file_exists():
  295. shutil.copy(self.filename, '{}.{}'.format(self.filename, time.strftime("%Y%m%dT%H%M%S")))
  296. # Try to set format attributes if supported
  297. try:
  298. self.yaml_dict.fa.set_block_style()
  299. except AttributeError:
  300. pass
  301. # Try to use RoundTripDumper if supported.
  302. if self.content_type == 'yaml':
  303. try:
  304. Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
  305. except AttributeError:
  306. Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
  307. elif self.content_type == 'json':
  308. Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
  309. else:
  310. raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
  311. 'Please specify a content_type of yaml or json.')
  312. return (True, self.yaml_dict)
  313. def read(self):
  314. ''' read from file '''
  315. # check if it exists
  316. if self.filename is None or not self.file_exists():
  317. return None
  318. contents = None
  319. with open(self.filename) as yfd:
  320. contents = yfd.read()
  321. return contents
  322. def file_exists(self):
  323. ''' return whether file exists '''
  324. if os.path.exists(self.filename):
  325. return True
  326. return False
  327. def load(self, content_type='yaml'):
  328. ''' return yaml file '''
  329. contents = self.read()
  330. if not contents and not self.content:
  331. return None
  332. if self.content:
  333. if isinstance(self.content, dict):
  334. self.yaml_dict = self.content
  335. return self.yaml_dict
  336. elif isinstance(self.content, str):
  337. contents = self.content
  338. # check if it is yaml
  339. try:
  340. if content_type == 'yaml' and contents:
  341. # Try to set format attributes if supported
  342. try:
  343. self.yaml_dict.fa.set_block_style()
  344. except AttributeError:
  345. pass
  346. # Try to use RoundTripLoader if supported.
  347. try:
  348. self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
  349. except AttributeError:
  350. self.yaml_dict = yaml.safe_load(contents)
  351. # Try to set format attributes if supported
  352. try:
  353. self.yaml_dict.fa.set_block_style()
  354. except AttributeError:
  355. pass
  356. elif content_type == 'json' and contents:
  357. self.yaml_dict = json.loads(contents)
  358. except yaml.YAMLError as err:
  359. # Error loading yaml or json
  360. raise YeditException('Problem with loading yaml file. {}'.format(err))
  361. return self.yaml_dict
  362. def get(self, key):
  363. ''' get a specified key'''
  364. try:
  365. entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
  366. except KeyError:
  367. entry = None
  368. return entry
  369. def pop(self, path, key_or_item):
  370. ''' remove a key, value pair from a dict or an item for a list'''
  371. try:
  372. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  373. except KeyError:
  374. entry = None
  375. if entry is None:
  376. return (False, self.yaml_dict)
  377. if isinstance(entry, dict):
  378. # AUDIT:maybe-no-member makes sense due to fuzzy types
  379. # pylint: disable=maybe-no-member
  380. if key_or_item in entry:
  381. entry.pop(key_or_item)
  382. return (True, self.yaml_dict)
  383. return (False, self.yaml_dict)
  384. elif isinstance(entry, list):
  385. # AUDIT:maybe-no-member makes sense due to fuzzy types
  386. # pylint: disable=maybe-no-member
  387. ind = None
  388. try:
  389. ind = entry.index(key_or_item)
  390. except ValueError:
  391. return (False, self.yaml_dict)
  392. entry.pop(ind)
  393. return (True, self.yaml_dict)
  394. return (False, self.yaml_dict)
  395. def delete(self, path, index=None, value=None):
  396. ''' remove path from a dict'''
  397. try:
  398. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  399. except KeyError:
  400. entry = None
  401. if entry is None:
  402. return (False, self.yaml_dict)
  403. result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
  404. if not result:
  405. return (False, self.yaml_dict)
  406. return (True, self.yaml_dict)
  407. def exists(self, path, value):
  408. ''' check if value exists at path'''
  409. try:
  410. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  411. except KeyError:
  412. entry = None
  413. if isinstance(entry, list):
  414. if value in entry:
  415. return True
  416. return False
  417. elif isinstance(entry, dict):
  418. if isinstance(value, dict):
  419. rval = False
  420. for key, val in value.items():
  421. if entry[key] != val:
  422. rval = False
  423. break
  424. else:
  425. rval = True
  426. return rval
  427. return value in entry
  428. return entry == value
  429. def append(self, path, value):
  430. '''append value to a list'''
  431. try:
  432. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  433. except KeyError:
  434. entry = None
  435. if entry is None:
  436. self.put(path, [])
  437. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  438. if not isinstance(entry, list):
  439. return (False, self.yaml_dict)
  440. # AUDIT:maybe-no-member makes sense due to loading data from
  441. # a serialized format.
  442. # pylint: disable=maybe-no-member
  443. entry.append(value)
  444. return (True, self.yaml_dict)
  445. # pylint: disable=too-many-arguments
  446. def update(self, path, value, index=None, curr_value=None):
  447. ''' put path, value into a dict '''
  448. try:
  449. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  450. except KeyError:
  451. entry = None
  452. if isinstance(entry, dict):
  453. # AUDIT:maybe-no-member makes sense due to fuzzy types
  454. # pylint: disable=maybe-no-member
  455. if not isinstance(value, dict):
  456. raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
  457. 'value=[{}] type=[{}]'.format(value, type(value)))
  458. entry.update(value)
  459. return (True, self.yaml_dict)
  460. elif isinstance(entry, list):
  461. # AUDIT:maybe-no-member makes sense due to fuzzy types
  462. # pylint: disable=maybe-no-member
  463. ind = None
  464. if curr_value:
  465. try:
  466. ind = entry.index(curr_value)
  467. except ValueError:
  468. return (False, self.yaml_dict)
  469. elif index is not None:
  470. ind = index
  471. if ind is not None and entry[ind] != value:
  472. entry[ind] = value
  473. return (True, self.yaml_dict)
  474. # see if it exists in the list
  475. try:
  476. ind = entry.index(value)
  477. except ValueError:
  478. # doesn't exist, append it
  479. entry.append(value)
  480. return (True, self.yaml_dict)
  481. # already exists, return
  482. if ind is not None:
  483. return (False, self.yaml_dict)
  484. return (False, self.yaml_dict)
  485. def put(self, path, value):
  486. ''' put path, value into a dict '''
  487. try:
  488. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  489. except KeyError:
  490. entry = None
  491. if entry == value:
  492. return (False, self.yaml_dict)
  493. # deepcopy didn't work
  494. # Try to use ruamel.yaml and fallback to pyyaml
  495. try:
  496. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  497. default_flow_style=False),
  498. yaml.RoundTripLoader)
  499. except AttributeError:
  500. tmp_copy = copy.deepcopy(self.yaml_dict)
  501. # set the format attributes if available
  502. try:
  503. tmp_copy.fa.set_block_style()
  504. except AttributeError:
  505. pass
  506. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  507. if result is None:
  508. return (False, self.yaml_dict)
  509. # When path equals "" it is a special case.
  510. # "" refers to the root of the document
  511. # Only update the root path (entire document) when its a list or dict
  512. if path == '':
  513. if isinstance(result, list) or isinstance(result, dict):
  514. self.yaml_dict = result
  515. return (True, self.yaml_dict)
  516. return (False, self.yaml_dict)
  517. self.yaml_dict = tmp_copy
  518. return (True, self.yaml_dict)
  519. def create(self, path, value):
  520. ''' create a yaml file '''
  521. if not self.file_exists():
  522. # deepcopy didn't work
  523. # Try to use ruamel.yaml and fallback to pyyaml
  524. try:
  525. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  526. default_flow_style=False),
  527. yaml.RoundTripLoader)
  528. except AttributeError:
  529. tmp_copy = copy.deepcopy(self.yaml_dict)
  530. # set the format attributes if available
  531. try:
  532. tmp_copy.fa.set_block_style()
  533. except AttributeError:
  534. pass
  535. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  536. if result is not None:
  537. self.yaml_dict = tmp_copy
  538. return (True, self.yaml_dict)
  539. return (False, self.yaml_dict)
  540. @staticmethod
  541. def get_curr_value(invalue, val_type):
  542. '''return the current value'''
  543. if invalue is None:
  544. return None
  545. curr_value = invalue
  546. if val_type == 'yaml':
  547. try:
  548. # AUDIT:maybe-no-member makes sense due to different yaml libraries
  549. # pylint: disable=maybe-no-member
  550. curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
  551. except AttributeError:
  552. curr_value = yaml.safe_load(invalue)
  553. elif val_type == 'json':
  554. curr_value = json.loads(invalue)
  555. return curr_value
  556. @staticmethod
  557. def parse_value(inc_value, vtype=''):
  558. '''determine value type passed'''
  559. true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
  560. 'on', 'On', 'ON', ]
  561. false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
  562. 'off', 'Off', 'OFF']
  563. # It came in as a string but you didn't specify value_type as string
  564. # we will convert to bool if it matches any of the above cases
  565. if isinstance(inc_value, str) and 'bool' in vtype:
  566. if inc_value not in true_bools and inc_value not in false_bools:
  567. raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
  568. elif isinstance(inc_value, bool) and 'str' in vtype:
  569. inc_value = str(inc_value)
  570. # There is a special case where '' will turn into None after yaml loading it so skip
  571. if isinstance(inc_value, str) and inc_value == '':
  572. pass
  573. # If vtype is not str then go ahead and attempt to yaml load it.
  574. elif isinstance(inc_value, str) and 'str' not in vtype:
  575. try:
  576. inc_value = yaml.safe_load(inc_value)
  577. except Exception:
  578. raise YeditException('Could not determine type of incoming value. ' +
  579. 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
  580. return inc_value
  581. @staticmethod
  582. def process_edits(edits, yamlfile):
  583. '''run through a list of edits and process them one-by-one'''
  584. results = []
  585. for edit in edits:
  586. value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
  587. if edit.get('action') == 'update':
  588. # pylint: disable=line-too-long
  589. curr_value = Yedit.get_curr_value(
  590. Yedit.parse_value(edit.get('curr_value')),
  591. edit.get('curr_value_format'))
  592. rval = yamlfile.update(edit['key'],
  593. value,
  594. edit.get('index'),
  595. curr_value)
  596. elif edit.get('action') == 'append':
  597. rval = yamlfile.append(edit['key'], value)
  598. else:
  599. rval = yamlfile.put(edit['key'], value)
  600. if rval[0]:
  601. results.append({'key': edit['key'], 'edit': rval[1]})
  602. return {'changed': len(results) > 0, 'results': results}
  603. # pylint: disable=too-many-return-statements,too-many-branches
  604. @staticmethod
  605. def run_ansible(params):
  606. '''perform the idempotent crud operations'''
  607. yamlfile = Yedit(filename=params['src'],
  608. backup=params['backup'],
  609. content_type=params['content_type'],
  610. separator=params['separator'])
  611. state = params['state']
  612. if params['src']:
  613. rval = yamlfile.load()
  614. if yamlfile.yaml_dict is None and state != 'present':
  615. return {'failed': True,
  616. 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
  617. 'file exists, that it is has correct permissions, and is valid yaml.'}
  618. if state == 'list':
  619. if params['content']:
  620. content = Yedit.parse_value(params['content'], params['content_type'])
  621. yamlfile.yaml_dict = content
  622. if params['key']:
  623. rval = yamlfile.get(params['key'])
  624. return {'changed': False, 'result': rval, 'state': state}
  625. elif state == 'absent':
  626. if params['content']:
  627. content = Yedit.parse_value(params['content'], params['content_type'])
  628. yamlfile.yaml_dict = content
  629. if params['update']:
  630. rval = yamlfile.pop(params['key'], params['value'])
  631. else:
  632. rval = yamlfile.delete(params['key'], params['index'], params['value'])
  633. if rval[0] and params['src']:
  634. yamlfile.write()
  635. return {'changed': rval[0], 'result': rval[1], 'state': state}
  636. elif state == 'present':
  637. # check if content is different than what is in the file
  638. if params['content']:
  639. content = Yedit.parse_value(params['content'], params['content_type'])
  640. # We had no edits to make and the contents are the same
  641. if yamlfile.yaml_dict == content and \
  642. params['value'] is None:
  643. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  644. yamlfile.yaml_dict = content
  645. # If we were passed a key, value then
  646. # we enapsulate it in a list and process it
  647. # Key, Value passed to the module : Converted to Edits list #
  648. edits = []
  649. _edit = {}
  650. if params['value'] is not None:
  651. _edit['value'] = params['value']
  652. _edit['value_type'] = params['value_type']
  653. _edit['key'] = params['key']
  654. if params['update']:
  655. _edit['action'] = 'update'
  656. _edit['curr_value'] = params['curr_value']
  657. _edit['curr_value_format'] = params['curr_value_format']
  658. _edit['index'] = params['index']
  659. elif params['append']:
  660. _edit['action'] = 'append'
  661. edits.append(_edit)
  662. elif params['edits'] is not None:
  663. edits = params['edits']
  664. if edits:
  665. results = Yedit.process_edits(edits, yamlfile)
  666. # if there were changes and a src provided to us we need to write
  667. if results['changed'] and params['src']:
  668. yamlfile.write()
  669. return {'changed': results['changed'], 'result': results['results'], 'state': state}
  670. # no edits to make
  671. if params['src']:
  672. # pylint: disable=redefined-variable-type
  673. rval = yamlfile.write()
  674. return {'changed': rval[0],
  675. 'result': rval[1],
  676. 'state': state}
  677. # We were passed content but no src, key or value, or edits. Return contents in memory
  678. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  679. return {'failed': True, 'msg': 'Unkown state passed'}
  680. # -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
  681. # -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
  682. # pylint: disable=too-many-lines
  683. # noqa: E301,E302,E303,T001
  684. class OpenShiftCLIError(Exception):
  685. '''Exception class for openshiftcli'''
  686. pass
  687. ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
  688. def locate_oc_binary():
  689. ''' Find and return oc binary file '''
  690. # https://github.com/openshift/openshift-ansible/issues/3410
  691. # oc can be in /usr/local/bin in some cases, but that may not
  692. # be in $PATH due to ansible/sudo
  693. paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
  694. oc_binary = 'oc'
  695. # Use shutil.which if it is available, otherwise fallback to a naive path search
  696. try:
  697. which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
  698. if which_result is not None:
  699. oc_binary = which_result
  700. except AttributeError:
  701. for path in paths:
  702. if os.path.exists(os.path.join(path, oc_binary)):
  703. oc_binary = os.path.join(path, oc_binary)
  704. break
  705. return oc_binary
  706. # pylint: disable=too-few-public-methods
  707. class OpenShiftCLI(object):
  708. ''' Class to wrap the command line tools '''
  709. def __init__(self,
  710. namespace,
  711. kubeconfig='/etc/origin/master/admin.kubeconfig',
  712. verbose=False,
  713. all_namespaces=False):
  714. ''' Constructor for OpenshiftCLI '''
  715. self.namespace = namespace
  716. self.verbose = verbose
  717. self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
  718. self.all_namespaces = all_namespaces
  719. self.oc_binary = locate_oc_binary()
  720. # Pylint allows only 5 arguments to be passed.
  721. # pylint: disable=too-many-arguments
  722. def _replace_content(self, resource, rname, content, edits=None, force=False, sep='.'):
  723. ''' replace the current object with the content '''
  724. res = self._get(resource, rname)
  725. if not res['results']:
  726. return res
  727. fname = Utils.create_tmpfile(rname + '-')
  728. yed = Yedit(fname, res['results'][0], separator=sep)
  729. updated = False
  730. if content is not None:
  731. changes = []
  732. for key, value in content.items():
  733. changes.append(yed.put(key, value))
  734. if any([change[0] for change in changes]):
  735. updated = True
  736. elif edits is not None:
  737. results = Yedit.process_edits(edits, yed)
  738. if results['changed']:
  739. updated = True
  740. if updated:
  741. yed.write()
  742. atexit.register(Utils.cleanup, [fname])
  743. return self._replace(fname, force)
  744. return {'returncode': 0, 'updated': False}
  745. def _replace(self, fname, force=False):
  746. '''replace the current object with oc replace'''
  747. # We are removing the 'resourceVersion' to handle
  748. # a race condition when modifying oc objects
  749. yed = Yedit(fname)
  750. results = yed.delete('metadata.resourceVersion')
  751. if results[0]:
  752. yed.write()
  753. cmd = ['replace', '-f', fname]
  754. if force:
  755. cmd.append('--force')
  756. return self.openshift_cmd(cmd)
  757. def _create_from_content(self, rname, content):
  758. '''create a temporary file and then call oc create on it'''
  759. fname = Utils.create_tmpfile(rname + '-')
  760. yed = Yedit(fname, content=content)
  761. yed.write()
  762. atexit.register(Utils.cleanup, [fname])
  763. return self._create(fname)
  764. def _create(self, fname):
  765. '''call oc create on a filename'''
  766. return self.openshift_cmd(['create', '-f', fname])
  767. def _delete(self, resource, name=None, selector=None):
  768. '''call oc delete on a resource'''
  769. cmd = ['delete', resource]
  770. if selector is not None:
  771. cmd.append('--selector={}'.format(selector))
  772. elif name is not None:
  773. cmd.append(name)
  774. else:
  775. raise OpenShiftCLIError('Either name or selector is required when calling delete.')
  776. return self.openshift_cmd(cmd)
  777. def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
  778. '''process a template
  779. template_name: the name of the template to process
  780. create: whether to send to oc create after processing
  781. params: the parameters for the template
  782. template_data: the incoming template's data; instead of a file
  783. '''
  784. cmd = ['process']
  785. if template_data:
  786. cmd.extend(['-f', '-'])
  787. else:
  788. cmd.append(template_name)
  789. if params:
  790. param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
  791. cmd.append('-v')
  792. cmd.extend(param_str)
  793. results = self.openshift_cmd(cmd, output=True, input_data=template_data)
  794. if results['returncode'] != 0 or not create:
  795. return results
  796. fname = Utils.create_tmpfile(template_name + '-')
  797. yed = Yedit(fname, results['results'])
  798. yed.write()
  799. atexit.register(Utils.cleanup, [fname])
  800. return self.openshift_cmd(['create', '-f', fname])
  801. def _get(self, resource, name=None, selector=None, field_selector=None):
  802. '''return a resource by name '''
  803. cmd = ['get', resource]
  804. if selector is not None:
  805. cmd.append('--selector={}'.format(selector))
  806. if field_selector is not None:
  807. cmd.append('--field-selector={}'.format(field_selector))
  808. # Name cannot be used with selector or field_selector.
  809. if selector is None and field_selector is None and name is not None:
  810. cmd.append(name)
  811. cmd.extend(['-o', 'json'])
  812. rval = self.openshift_cmd(cmd, output=True)
  813. # Ensure results are retuned in an array
  814. if 'items' in rval:
  815. rval['results'] = rval['items']
  816. elif not isinstance(rval['results'], list):
  817. rval['results'] = [rval['results']]
  818. return rval
  819. def _schedulable(self, node=None, selector=None, schedulable=True):
  820. ''' perform oadm manage-node scheduable '''
  821. cmd = ['manage-node']
  822. if node:
  823. cmd.extend(node)
  824. else:
  825. cmd.append('--selector={}'.format(selector))
  826. cmd.append('--schedulable={}'.format(schedulable))
  827. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
  828. def _list_pods(self, node=None, selector=None, pod_selector=None):
  829. ''' perform oadm list pods
  830. node: the node in which to list pods
  831. selector: the label selector filter if provided
  832. pod_selector: the pod selector filter if provided
  833. '''
  834. cmd = ['manage-node']
  835. if node:
  836. cmd.extend(node)
  837. else:
  838. cmd.append('--selector={}'.format(selector))
  839. if pod_selector:
  840. cmd.append('--pod-selector={}'.format(pod_selector))
  841. cmd.extend(['--list-pods', '-o', 'json'])
  842. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  843. # pylint: disable=too-many-arguments
  844. def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
  845. ''' perform oadm manage-node evacuate '''
  846. cmd = ['manage-node']
  847. if node:
  848. cmd.extend(node)
  849. else:
  850. cmd.append('--selector={}'.format(selector))
  851. if dry_run:
  852. cmd.append('--dry-run')
  853. if pod_selector:
  854. cmd.append('--pod-selector={}'.format(pod_selector))
  855. if grace_period:
  856. cmd.append('--grace-period={}'.format(int(grace_period)))
  857. if force:
  858. cmd.append('--force')
  859. cmd.append('--evacuate')
  860. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  861. def _version(self):
  862. ''' return the openshift version'''
  863. return self.openshift_cmd(['version'], output=True, output_type='raw')
  864. def _import_image(self, url=None, name=None, tag=None):
  865. ''' perform image import '''
  866. cmd = ['import-image']
  867. image = '{0}'.format(name)
  868. if tag:
  869. image += ':{0}'.format(tag)
  870. cmd.append(image)
  871. if url:
  872. cmd.append('--from={0}/{1}'.format(url, image))
  873. cmd.append('-n{0}'.format(self.namespace))
  874. cmd.append('--confirm')
  875. return self.openshift_cmd(cmd)
  876. def _run(self, cmds, input_data):
  877. ''' Actually executes the command. This makes mocking easier. '''
  878. curr_env = os.environ.copy()
  879. curr_env.update({'KUBECONFIG': self.kubeconfig})
  880. proc = subprocess.Popen(cmds,
  881. stdin=subprocess.PIPE,
  882. stdout=subprocess.PIPE,
  883. stderr=subprocess.PIPE,
  884. env=curr_env)
  885. stdout, stderr = proc.communicate(input_data)
  886. return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
  887. # pylint: disable=too-many-arguments,too-many-branches
  888. def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
  889. '''Base command for oc '''
  890. cmds = [self.oc_binary]
  891. if oadm:
  892. cmds.append('adm')
  893. cmds.extend(cmd)
  894. if self.all_namespaces:
  895. cmds.extend(['--all-namespaces'])
  896. elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
  897. cmds.extend(['-n', self.namespace])
  898. if self.verbose:
  899. print(' '.join(cmds))
  900. try:
  901. returncode, stdout, stderr = self._run(cmds, input_data)
  902. except OSError as ex:
  903. returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
  904. rval = {"returncode": returncode,
  905. "cmd": ' '.join(cmds)}
  906. if output_type == 'json':
  907. rval['results'] = {}
  908. if output and stdout:
  909. try:
  910. rval['results'] = json.loads(stdout)
  911. except ValueError as verr:
  912. if "No JSON object could be decoded" in verr.args:
  913. rval['err'] = verr.args
  914. elif output_type == 'raw':
  915. rval['results'] = stdout if output else ''
  916. if self.verbose:
  917. print("STDOUT: {0}".format(stdout))
  918. print("STDERR: {0}".format(stderr))
  919. if 'err' in rval or returncode != 0:
  920. rval.update({"stderr": stderr,
  921. "stdout": stdout})
  922. return rval
  923. class Utils(object): # pragma: no cover
  924. ''' utilities for openshiftcli modules '''
  925. @staticmethod
  926. def _write(filename, contents):
  927. ''' Actually write the file contents to disk. This helps with mocking. '''
  928. with open(filename, 'w') as sfd:
  929. sfd.write(str(contents))
  930. @staticmethod
  931. def create_tmp_file_from_contents(rname, data, ftype='yaml'):
  932. ''' create a file in tmp with name and contents'''
  933. tmp = Utils.create_tmpfile(prefix=rname)
  934. if ftype == 'yaml':
  935. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  936. # pylint: disable=no-member
  937. if hasattr(yaml, 'RoundTripDumper'):
  938. Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
  939. else:
  940. Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
  941. elif ftype == 'json':
  942. Utils._write(tmp, json.dumps(data))
  943. else:
  944. Utils._write(tmp, data)
  945. # Register cleanup when module is done
  946. atexit.register(Utils.cleanup, [tmp])
  947. return tmp
  948. @staticmethod
  949. def create_tmpfile_copy(inc_file):
  950. '''create a temporary copy of a file'''
  951. tmpfile = Utils.create_tmpfile('lib_openshift-')
  952. Utils._write(tmpfile, open(inc_file).read())
  953. # Cleanup the tmpfile
  954. atexit.register(Utils.cleanup, [tmpfile])
  955. return tmpfile
  956. @staticmethod
  957. def create_tmpfile(prefix='tmp'):
  958. ''' Generates and returns a temporary file name '''
  959. with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
  960. return tmp.name
  961. @staticmethod
  962. def create_tmp_files_from_contents(content, content_type=None):
  963. '''Turn an array of dict: filename, content into a files array'''
  964. if not isinstance(content, list):
  965. content = [content]
  966. files = []
  967. for item in content:
  968. path = Utils.create_tmp_file_from_contents(item['path'] + '-',
  969. item['data'],
  970. ftype=content_type)
  971. files.append({'name': os.path.basename(item['path']),
  972. 'path': path})
  973. return files
  974. @staticmethod
  975. def cleanup(files):
  976. '''Clean up on exit '''
  977. for sfile in files:
  978. if os.path.exists(sfile):
  979. if os.path.isdir(sfile):
  980. shutil.rmtree(sfile)
  981. elif os.path.isfile(sfile):
  982. os.remove(sfile)
  983. @staticmethod
  984. def exists(results, _name):
  985. ''' Check to see if the results include the name '''
  986. if not results:
  987. return False
  988. if Utils.find_result(results, _name):
  989. return True
  990. return False
  991. @staticmethod
  992. def find_result(results, _name):
  993. ''' Find the specified result by name'''
  994. rval = None
  995. for result in results:
  996. if 'metadata' in result and result['metadata']['name'] == _name:
  997. rval = result
  998. break
  999. return rval
  1000. @staticmethod
  1001. def get_resource_file(sfile, sfile_type='yaml'):
  1002. ''' return the service file '''
  1003. contents = None
  1004. with open(sfile) as sfd:
  1005. contents = sfd.read()
  1006. if sfile_type == 'yaml':
  1007. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  1008. # pylint: disable=no-member
  1009. if hasattr(yaml, 'RoundTripLoader'):
  1010. contents = yaml.load(contents, yaml.RoundTripLoader)
  1011. else:
  1012. contents = yaml.safe_load(contents)
  1013. elif sfile_type == 'json':
  1014. contents = json.loads(contents)
  1015. return contents
  1016. @staticmethod
  1017. def filter_versions(stdout):
  1018. ''' filter the oc version output '''
  1019. version_dict = {}
  1020. version_search = ['oc', 'openshift', 'kubernetes']
  1021. for line in stdout.strip().split('\n'):
  1022. for term in version_search:
  1023. if not line:
  1024. continue
  1025. if line.startswith(term):
  1026. version_dict[term] = line.split()[-1]
  1027. # horrible hack to get openshift version in Openshift 3.2
  1028. # By default "oc version in 3.2 does not return an "openshift" version
  1029. if "openshift" not in version_dict:
  1030. version_dict["openshift"] = version_dict["oc"]
  1031. return version_dict
  1032. @staticmethod
  1033. def add_custom_versions(versions):
  1034. ''' create custom versions strings '''
  1035. versions_dict = {}
  1036. for tech, version in versions.items():
  1037. # clean up "-" from version
  1038. if "-" in version:
  1039. version = version.split("-")[0]
  1040. if version.startswith('v'):
  1041. versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
  1042. # "v3.3.0.33" is what we have, we want "3.3"
  1043. versions_dict[tech + '_short'] = version[1:4]
  1044. return versions_dict
  1045. @staticmethod
  1046. def openshift_installed():
  1047. ''' check if openshift is installed '''
  1048. import rpm
  1049. transaction_set = rpm.TransactionSet()
  1050. rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
  1051. return rpmquery.count() > 0
  1052. # Disabling too-many-branches. This is a yaml dictionary comparison function
  1053. # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
  1054. @staticmethod
  1055. def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
  1056. ''' Given a user defined definition, compare it with the results given back by our query. '''
  1057. # Currently these values are autogenerated and we do not need to check them
  1058. skip = ['metadata', 'status']
  1059. if skip_keys:
  1060. skip.extend(skip_keys)
  1061. for key, value in result_def.items():
  1062. if key in skip:
  1063. continue
  1064. # Both are lists
  1065. if isinstance(value, list):
  1066. if key not in user_def:
  1067. if debug:
  1068. print('User data does not have key [%s]' % key)
  1069. print('User data: %s' % user_def)
  1070. return False
  1071. if not isinstance(user_def[key], list):
  1072. if debug:
  1073. print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
  1074. return False
  1075. if len(user_def[key]) != len(value):
  1076. if debug:
  1077. print("List lengths are not equal.")
  1078. print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
  1079. print("user_def: %s" % user_def[key])
  1080. print("value: %s" % value)
  1081. return False
  1082. for values in zip(user_def[key], value):
  1083. if isinstance(values[0], dict) and isinstance(values[1], dict):
  1084. if debug:
  1085. print('sending list - list')
  1086. print(type(values[0]))
  1087. print(type(values[1]))
  1088. result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
  1089. if not result:
  1090. print('list compare returned false')
  1091. return False
  1092. elif value != user_def[key]:
  1093. if debug:
  1094. print('value should be identical')
  1095. print(user_def[key])
  1096. print(value)
  1097. return False
  1098. # recurse on a dictionary
  1099. elif isinstance(value, dict):
  1100. if key not in user_def:
  1101. if debug:
  1102. print("user_def does not have key [%s]" % key)
  1103. return False
  1104. if not isinstance(user_def[key], dict):
  1105. if debug:
  1106. print("dict returned false: not instance of dict")
  1107. return False
  1108. # before passing ensure keys match
  1109. api_values = set(value.keys()) - set(skip)
  1110. user_values = set(user_def[key].keys()) - set(skip)
  1111. if api_values != user_values:
  1112. if debug:
  1113. print("keys are not equal in dict")
  1114. print(user_values)
  1115. print(api_values)
  1116. return False
  1117. result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
  1118. if not result:
  1119. if debug:
  1120. print("dict returned false")
  1121. print(result)
  1122. return False
  1123. # Verify each key, value pair is the same
  1124. else:
  1125. if key not in user_def or value != user_def[key]:
  1126. if debug:
  1127. print("value not equal; user_def does not have key")
  1128. print(key)
  1129. print(value)
  1130. if key in user_def:
  1131. print(user_def[key])
  1132. return False
  1133. if debug:
  1134. print('returning true')
  1135. return True
  1136. class OpenShiftCLIConfig(object):
  1137. '''Generic Config'''
  1138. def __init__(self, rname, namespace, kubeconfig, options):
  1139. self.kubeconfig = kubeconfig
  1140. self.name = rname
  1141. self.namespace = namespace
  1142. self._options = options
  1143. @property
  1144. def config_options(self):
  1145. ''' return config options '''
  1146. return self._options
  1147. def to_option_list(self, ascommalist=''):
  1148. '''return all options as a string
  1149. if ascommalist is set to the name of a key, and
  1150. the value of that key is a dict, format the dict
  1151. as a list of comma delimited key=value pairs'''
  1152. return self.stringify(ascommalist)
  1153. def stringify(self, ascommalist=''):
  1154. ''' return the options hash as cli params in a string
  1155. if ascommalist is set to the name of a key, and
  1156. the value of that key is a dict, format the dict
  1157. as a list of comma delimited key=value pairs '''
  1158. rval = []
  1159. for key in sorted(self.config_options.keys()):
  1160. data = self.config_options[key]
  1161. if data['include'] \
  1162. and (data['value'] is not None or isinstance(data['value'], int)):
  1163. if key == ascommalist:
  1164. val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
  1165. else:
  1166. val = data['value']
  1167. rval.append('--{}={}'.format(key.replace('_', '-'), val))
  1168. return rval
  1169. # -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
  1170. # -*- -*- -*- Begin included fragment: lib/rule.py -*- -*- -*-
  1171. class Rule(object):
  1172. '''class to represent a clusterrole rule
  1173. Example Rule Object's yaml:
  1174. - apiGroups:
  1175. - ""
  1176. attributeRestrictions: null
  1177. resources:
  1178. - persistentvolumes
  1179. verbs:
  1180. - create
  1181. - delete
  1182. - deletecollection
  1183. - get
  1184. - list
  1185. - patch
  1186. - update
  1187. - watch
  1188. '''
  1189. def __init__(self,
  1190. api_groups=None,
  1191. attr_restrictions=None,
  1192. resources=None,
  1193. verbs=None):
  1194. self.__api_groups = api_groups if api_groups is not None else [""]
  1195. self.__verbs = verbs if verbs is not None else []
  1196. self.__resources = resources if resources is not None else []
  1197. self.__attribute_restrictions = attr_restrictions if attr_restrictions is not None else None
  1198. @property
  1199. def verbs(self):
  1200. '''property for verbs'''
  1201. if self.__verbs is None:
  1202. return []
  1203. return self.__verbs
  1204. @verbs.setter
  1205. def verbs(self, data):
  1206. '''setter for verbs'''
  1207. self.__verbs = data
  1208. @property
  1209. def api_groups(self):
  1210. '''property for api_groups'''
  1211. if self.__api_groups is None:
  1212. return []
  1213. return self.__api_groups
  1214. @api_groups.setter
  1215. def api_groups(self, data):
  1216. '''setter for api_groups'''
  1217. self.__api_groups = data
  1218. @property
  1219. def resources(self):
  1220. '''property for resources'''
  1221. if self.__resources is None:
  1222. return []
  1223. return self.__resources
  1224. @resources.setter
  1225. def resources(self, data):
  1226. '''setter for resources'''
  1227. self.__resources = data
  1228. @property
  1229. def attribute_restrictions(self):
  1230. '''property for attribute_restrictions'''
  1231. return self.__attribute_restrictions
  1232. @attribute_restrictions.setter
  1233. def attribute_restrictions(self, data):
  1234. '''setter for attribute_restrictions'''
  1235. self.__attribute_restrictions = data
  1236. def add_verb(self, inc_verb):
  1237. '''add a verb to the verbs array'''
  1238. self.verbs.append(inc_verb)
  1239. def add_api_group(self, inc_apigroup):
  1240. '''add an api_group to the api_groups array'''
  1241. self.api_groups.append(inc_apigroup)
  1242. def add_resource(self, inc_resource):
  1243. '''add an resource to the resources array'''
  1244. self.resources.append(inc_resource)
  1245. def remove_verb(self, inc_verb):
  1246. '''add a verb to the verbs array'''
  1247. try:
  1248. self.verbs.remove(inc_verb)
  1249. return True
  1250. except ValueError:
  1251. pass
  1252. return False
  1253. def remove_api_group(self, inc_api_group):
  1254. '''add a verb to the verbs array'''
  1255. try:
  1256. self.api_groups.remove(inc_api_group)
  1257. return True
  1258. except ValueError:
  1259. pass
  1260. return False
  1261. def remove_resource(self, inc_resource):
  1262. '''add a verb to the verbs array'''
  1263. try:
  1264. self.resources.remove(inc_resource)
  1265. return True
  1266. except ValueError:
  1267. pass
  1268. return False
  1269. def __eq__(self, other):
  1270. '''return whether rules are equal'''
  1271. return (self.attribute_restrictions == other.attribute_restrictions and
  1272. self.api_groups == other.api_groups and
  1273. self.resources == other.resources and
  1274. self.verbs == other.verbs)
  1275. @staticmethod
  1276. def parse_rules(inc_rules):
  1277. '''create rules from an array'''
  1278. results = []
  1279. for rule in inc_rules:
  1280. results.append(Rule(rule.get('apiGroups', ['']),
  1281. rule.get('attributeRestrictions', None),
  1282. rule.get('resources', []),
  1283. rule.get('verbs', [])))
  1284. return results
  1285. # -*- -*- -*- End included fragment: lib/rule.py -*- -*- -*-
  1286. # -*- -*- -*- Begin included fragment: lib/clusterrole.py -*- -*- -*-
  1287. # pylint: disable=too-many-public-methods
  1288. class ClusterRole(Yedit):
  1289. ''' Class to model an openshift ClusterRole'''
  1290. rules_path = "rules"
  1291. def __init__(self, name=None, content=None):
  1292. ''' Constructor for clusterrole '''
  1293. if content is None:
  1294. content = ClusterRole.builder(name).yaml_dict
  1295. super(ClusterRole, self).__init__(content=content)
  1296. self.__rules = Rule.parse_rules(self.get(ClusterRole.rules_path)) or []
  1297. @property
  1298. def rules(self):
  1299. return self.__rules
  1300. @rules.setter
  1301. def rules(self, data):
  1302. self.__rules = data
  1303. self.put(ClusterRole.rules_path, self.__rules)
  1304. def rule_exists(self, inc_rule):
  1305. '''attempt to find the inc_rule in the rules list'''
  1306. for rule in self.rules:
  1307. if rule == inc_rule:
  1308. return True
  1309. return False
  1310. def compare(self, other, verbose=False):
  1311. '''compare function for clusterrole'''
  1312. for rule in other.rules:
  1313. if rule not in self.rules:
  1314. if verbose:
  1315. print('Rule in other not found in self. [{}]'.format(rule))
  1316. return False
  1317. for rule in self.rules:
  1318. if rule not in other.rules:
  1319. if verbose:
  1320. print('Rule in self not found in other. [{}]'.format(rule))
  1321. return False
  1322. return True
  1323. @staticmethod
  1324. def builder(name='default_clusterrole', rules=None):
  1325. '''return a clusterrole with name and/or rules'''
  1326. if rules is None:
  1327. rules = [{'apiGroups': [""],
  1328. 'attributeRestrictions': None,
  1329. 'verbs': [],
  1330. 'resources': []}]
  1331. content = {
  1332. 'apiVersion': 'v1',
  1333. 'kind': 'ClusterRole',
  1334. 'metadata': {'name': '{}'.format(name)},
  1335. 'rules': rules,
  1336. }
  1337. return ClusterRole(content=content)
  1338. # -*- -*- -*- End included fragment: lib/clusterrole.py -*- -*- -*-
  1339. # -*- -*- -*- Begin included fragment: class/oc_clusterrole.py -*- -*- -*-
  1340. # pylint: disable=too-many-instance-attributes
  1341. class OCClusterRole(OpenShiftCLI):
  1342. ''' Class to manage clusterrole objects'''
  1343. kind = 'clusterrole'
  1344. def __init__(self,
  1345. name,
  1346. rules=None,
  1347. kubeconfig=None,
  1348. verbose=False):
  1349. ''' Constructor for OCClusterRole '''
  1350. super(OCClusterRole, self).__init__(None, kubeconfig=kubeconfig, verbose=verbose)
  1351. self.verbose = verbose
  1352. self.name = name
  1353. self._clusterrole = None
  1354. self._inc_clusterrole = ClusterRole.builder(name, rules)
  1355. @property
  1356. def clusterrole(self):
  1357. ''' property for clusterrole'''
  1358. if self._clusterrole is None:
  1359. self.get()
  1360. return self._clusterrole
  1361. @clusterrole.setter
  1362. def clusterrole(self, data):
  1363. ''' setter function for clusterrole property'''
  1364. self._clusterrole = data
  1365. @property
  1366. def inc_clusterrole(self):
  1367. ''' property for inc_clusterrole'''
  1368. return self._inc_clusterrole
  1369. @inc_clusterrole.setter
  1370. def inc_clusterrole(self, data):
  1371. ''' setter function for inc_clusterrole property'''
  1372. self._inc_clusterrole = data
  1373. def exists(self):
  1374. ''' return whether a clusterrole exists '''
  1375. if self.clusterrole:
  1376. return True
  1377. return False
  1378. def get(self):
  1379. '''return a clusterrole '''
  1380. result = self._get(self.kind, self.name)
  1381. if result['returncode'] == 0:
  1382. self.clusterrole = ClusterRole(content=result['results'][0])
  1383. result['results'] = self.clusterrole.yaml_dict
  1384. elif '"{}" not found'.format(self.name) in result['stderr']:
  1385. result['returncode'] = 0
  1386. self.clusterrole = None
  1387. return result
  1388. def delete(self):
  1389. '''delete the object'''
  1390. return self._delete(self.kind, self.name)
  1391. def create(self):
  1392. '''create a clusterrole from the proposed incoming clusterrole'''
  1393. return self._create_from_content(self.name, self.inc_clusterrole.yaml_dict)
  1394. def update(self):
  1395. '''update a project'''
  1396. return self._replace_content(self.kind, self.name, self.inc_clusterrole.yaml_dict)
  1397. def needs_update(self):
  1398. ''' verify an update is needed'''
  1399. return not self.clusterrole.compare(self.inc_clusterrole, self.verbose)
  1400. # pylint: disable=too-many-return-statements,too-many-branches
  1401. @staticmethod
  1402. def run_ansible(params, check_mode):
  1403. '''run the idempotent ansible code'''
  1404. oc_clusterrole = OCClusterRole(params['name'],
  1405. params['rules'],
  1406. params['kubeconfig'],
  1407. params['debug'])
  1408. state = params['state']
  1409. api_rval = oc_clusterrole.get()
  1410. #####
  1411. # Get
  1412. #####
  1413. if state == 'list':
  1414. return {'changed': False, 'results': api_rval, 'state': state}
  1415. ########
  1416. # Delete
  1417. ########
  1418. if state == 'absent':
  1419. if oc_clusterrole.exists():
  1420. if check_mode:
  1421. return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
  1422. api_rval = oc_clusterrole.delete()
  1423. if api_rval['returncode'] != 0:
  1424. return {'failed': True, 'msg': api_rval}
  1425. return {'changed': True, 'results': api_rval, 'state': state}
  1426. return {'changed': False, 'state': state}
  1427. if state == 'present':
  1428. ########
  1429. # Create
  1430. ########
  1431. if not oc_clusterrole.exists():
  1432. if check_mode:
  1433. return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
  1434. # Create it here
  1435. api_rval = oc_clusterrole.create()
  1436. if api_rval['returncode'] != 0:
  1437. return {'failed': True, 'msg': api_rval}
  1438. # return the created object
  1439. api_rval = oc_clusterrole.get()
  1440. if api_rval['returncode'] != 0:
  1441. return {'failed': True, 'msg': api_rval}
  1442. return {'changed': True, 'results': api_rval, 'state': state}
  1443. ########
  1444. # Update
  1445. ########
  1446. if oc_clusterrole.needs_update():
  1447. if check_mode:
  1448. return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'}
  1449. api_rval = oc_clusterrole.update()
  1450. if api_rval['returncode'] != 0:
  1451. return {'failed': True, 'msg': api_rval}
  1452. # return the created object
  1453. api_rval = oc_clusterrole.get()
  1454. if api_rval['returncode'] != 0:
  1455. return {'failed': True, 'msg': api_rval}
  1456. return {'changed': True, 'results': api_rval, 'state': state}
  1457. return {'changed': False, 'results': api_rval, 'state': state}
  1458. return {'failed': True,
  1459. 'changed': False,
  1460. 'msg': 'Unknown state passed. [%s]' % state}
  1461. # -*- -*- -*- End included fragment: class/oc_clusterrole.py -*- -*- -*-
  1462. # -*- -*- -*- Begin included fragment: ansible/oc_clusterrole.py -*- -*- -*-
  1463. def main():
  1464. '''
  1465. ansible oc module for clusterrole
  1466. '''
  1467. module = AnsibleModule(
  1468. argument_spec=dict(
  1469. kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
  1470. state=dict(default='present', type='str',
  1471. choices=['present', 'absent', 'list']),
  1472. debug=dict(default=False, type='bool'),
  1473. name=dict(default=None, type='str'),
  1474. rules=dict(default=None, type='list'),
  1475. ),
  1476. supports_check_mode=True,
  1477. )
  1478. results = OCClusterRole.run_ansible(module.params, module.check_mode)
  1479. if 'failed' in results:
  1480. module.fail_json(**results)
  1481. module.exit_json(**results)
  1482. if __name__ == '__main__':
  1483. main()
  1484. # -*- -*- -*- End included fragment: ansible/oc_clusterrole.py -*- -*- -*-