oc_serviceaccount_secret.py 59 KB

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