oc_scale.py 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972
  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/scale -*- -*- -*-
  51. DOCUMENTATION = '''
  52. ---
  53. module: oc_scale
  54. short_description: Manage openshift services through the scale parameters
  55. description:
  56. - Manage openshift services through scaling them.
  57. options:
  58. state:
  59. description:
  60. - State represents whether to scale or list the current replicas
  61. required: true
  62. default: present
  63. choices: ["present", "list"]
  64. aliases: []
  65. kubeconfig:
  66. description:
  67. - The path for the kubeconfig file to use for authentication
  68. required: false
  69. default: /etc/origin/master/admin.kubeconfig
  70. aliases: []
  71. debug:
  72. description:
  73. - Turn on debug output.
  74. required: false
  75. default: False
  76. aliases: []
  77. name:
  78. description:
  79. - Name of the object that is being queried.
  80. required: false
  81. default: None
  82. aliases: []
  83. namespace:
  84. description:
  85. - The namespace where the object lives.
  86. required: false
  87. default: default
  88. aliases: []
  89. kind:
  90. description:
  91. - The kind of object to scale.
  92. required: false
  93. default: None
  94. choices:
  95. - rc
  96. - dc
  97. aliases: []
  98. author:
  99. - "Kenny Woodson <kwoodson@redhat.com>"
  100. extends_documentation_fragment: []
  101. '''
  102. EXAMPLES = '''
  103. - name: scale down a rc to 0
  104. oc_scale:
  105. name: my-replication-controller
  106. kind: rc
  107. namespace: openshift-infra
  108. replicas: 0
  109. - name: scale up a deploymentconfig to 2
  110. oc_scale:
  111. name: php
  112. kind: dc
  113. namespace: my-php-app
  114. replicas: 2
  115. '''
  116. # -*- -*- -*- End included fragment: doc/scale -*- -*- -*-
  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
  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=False):
  134. self.content = content
  135. self._separator = separator
  136. self.filename = filename
  137. self.__yaml_dict = content
  138. self.content_type = content_type
  139. self.backup = backup
  140. self.load(content_type=self.content_type)
  141. if self.__yaml_dict is None:
  142. self.__yaml_dict = {}
  143. @property
  144. def separator(self):
  145. ''' getter method for separator '''
  146. return self._separator
  147. @separator.setter
  148. def separator(self, inc_sep):
  149. ''' setter method for separator '''
  150. self._separator = inc_sep
  151. @property
  152. def yaml_dict(self):
  153. ''' getter method for yaml_dict '''
  154. return self.__yaml_dict
  155. @yaml_dict.setter
  156. def yaml_dict(self, value):
  157. ''' setter method for yaml_dict '''
  158. self.__yaml_dict = value
  159. @staticmethod
  160. def parse_key(key, sep='.'):
  161. '''parse the key allowing the appropriate separator'''
  162. common_separators = list(Yedit.com_sep - set([sep]))
  163. return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
  164. @staticmethod
  165. def valid_key(key, sep='.'):
  166. '''validate the incoming key'''
  167. common_separators = list(Yedit.com_sep - set([sep]))
  168. if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
  169. return False
  170. return True
  171. # pylint: disable=too-many-return-statements,too-many-branches
  172. @staticmethod
  173. def remove_entry(data, key, index=None, value=None, sep='.'):
  174. ''' remove data at location key '''
  175. if key == '' and isinstance(data, dict):
  176. if value is not None:
  177. data.pop(value)
  178. elif index is not None:
  179. raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
  180. else:
  181. data.clear()
  182. return True
  183. elif key == '' and isinstance(data, list):
  184. ind = None
  185. if value is not None:
  186. try:
  187. ind = data.index(value)
  188. except ValueError:
  189. return False
  190. elif index is not None:
  191. ind = index
  192. else:
  193. del data[:]
  194. if ind is not None:
  195. data.pop(ind)
  196. return True
  197. if not (key and Yedit.valid_key(key, sep)) and \
  198. isinstance(data, (list, dict)):
  199. return None
  200. key_indexes = Yedit.parse_key(key, sep)
  201. for arr_ind, dict_key in key_indexes[:-1]:
  202. if dict_key and isinstance(data, dict):
  203. data = data.get(dict_key)
  204. elif (arr_ind and isinstance(data, list) and
  205. int(arr_ind) <= len(data) - 1):
  206. data = data[int(arr_ind)]
  207. else:
  208. return None
  209. # process last index for remove
  210. # expected list entry
  211. if key_indexes[-1][0]:
  212. if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  213. del data[int(key_indexes[-1][0])]
  214. return True
  215. # expected dict entry
  216. elif key_indexes[-1][1]:
  217. if isinstance(data, dict):
  218. del data[key_indexes[-1][1]]
  219. return True
  220. @staticmethod
  221. def add_entry(data, key, item=None, sep='.'):
  222. ''' Get an item from a dictionary with key notation a.b.c
  223. d = {'a': {'b': 'c'}}}
  224. key = a#b
  225. return c
  226. '''
  227. if key == '':
  228. pass
  229. elif (not (key and Yedit.valid_key(key, sep)) and
  230. isinstance(data, (list, dict))):
  231. return None
  232. key_indexes = Yedit.parse_key(key, sep)
  233. for arr_ind, dict_key in key_indexes[:-1]:
  234. if dict_key:
  235. if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
  236. data = data[dict_key]
  237. continue
  238. elif data and not isinstance(data, dict):
  239. raise YeditException("Unexpected item type found while going through key " +
  240. "path: {} (at key: {})".format(key, dict_key))
  241. data[dict_key] = {}
  242. data = data[dict_key]
  243. elif (arr_ind and isinstance(data, list) and
  244. int(arr_ind) <= len(data) - 1):
  245. data = data[int(arr_ind)]
  246. else:
  247. raise YeditException("Unexpected item type found while going through key path: {}".format(key))
  248. if key == '':
  249. data = item
  250. # process last index for add
  251. # expected list entry
  252. elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  253. data[int(key_indexes[-1][0])] = item
  254. # expected dict entry
  255. elif key_indexes[-1][1] and isinstance(data, dict):
  256. data[key_indexes[-1][1]] = item
  257. # didn't add/update to an existing list, nor add/update key to a dict
  258. # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
  259. # non-existent array
  260. else:
  261. raise YeditException("Error adding to object at path: {}".format(key))
  262. return data
  263. @staticmethod
  264. def get_entry(data, key, sep='.'):
  265. ''' Get an item from a dictionary with key notation a.b.c
  266. d = {'a': {'b': 'c'}}}
  267. key = a.b
  268. return c
  269. '''
  270. if key == '':
  271. pass
  272. elif (not (key and Yedit.valid_key(key, sep)) and
  273. isinstance(data, (list, dict))):
  274. return None
  275. key_indexes = Yedit.parse_key(key, sep)
  276. for arr_ind, dict_key in key_indexes:
  277. if dict_key and isinstance(data, dict):
  278. data = data.get(dict_key)
  279. elif (arr_ind and isinstance(data, list) and
  280. int(arr_ind) <= len(data) - 1):
  281. data = data[int(arr_ind)]
  282. else:
  283. return None
  284. return data
  285. @staticmethod
  286. def _write(filename, contents):
  287. ''' Actually write the file contents to disk. This helps with mocking. '''
  288. tmp_filename = filename + '.yedit'
  289. with open(tmp_filename, 'w') as yfd:
  290. fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  291. yfd.write(contents)
  292. fcntl.flock(yfd, fcntl.LOCK_UN)
  293. os.rename(tmp_filename, filename)
  294. def write(self):
  295. ''' write to file '''
  296. if not self.filename:
  297. raise YeditException('Please specify a filename.')
  298. if self.backup and self.file_exists():
  299. shutil.copy(self.filename, '{}.{}'.format(self.filename, time.strftime("%Y%m%dT%H%M%S")))
  300. # Try to set format attributes if supported
  301. try:
  302. self.yaml_dict.fa.set_block_style()
  303. except AttributeError:
  304. pass
  305. # Try to use RoundTripDumper if supported.
  306. if self.content_type == 'yaml':
  307. try:
  308. Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
  309. except AttributeError:
  310. Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
  311. elif self.content_type == 'json':
  312. Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
  313. else:
  314. raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
  315. 'Please specify a content_type of yaml or json.')
  316. return (True, self.yaml_dict)
  317. def read(self):
  318. ''' read from file '''
  319. # check if it exists
  320. if self.filename is None or not self.file_exists():
  321. return None
  322. contents = None
  323. with open(self.filename) as yfd:
  324. contents = yfd.read()
  325. return contents
  326. def file_exists(self):
  327. ''' return whether file exists '''
  328. if os.path.exists(self.filename):
  329. return True
  330. return False
  331. def load(self, content_type='yaml'):
  332. ''' return yaml file '''
  333. contents = self.read()
  334. if not contents and not self.content:
  335. return None
  336. if self.content:
  337. if isinstance(self.content, dict):
  338. self.yaml_dict = self.content
  339. return self.yaml_dict
  340. elif isinstance(self.content, str):
  341. contents = self.content
  342. # check if it is yaml
  343. try:
  344. if content_type == 'yaml' and contents:
  345. # Try to set format attributes if supported
  346. try:
  347. self.yaml_dict.fa.set_block_style()
  348. except AttributeError:
  349. pass
  350. # Try to use RoundTripLoader if supported.
  351. try:
  352. self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
  353. except AttributeError:
  354. self.yaml_dict = yaml.safe_load(contents)
  355. # Try to set format attributes if supported
  356. try:
  357. self.yaml_dict.fa.set_block_style()
  358. except AttributeError:
  359. pass
  360. elif content_type == 'json' and contents:
  361. self.yaml_dict = json.loads(contents)
  362. except yaml.YAMLError as err:
  363. # Error loading yaml or json
  364. raise YeditException('Problem with loading yaml file. {}'.format(err))
  365. return self.yaml_dict
  366. def get(self, key):
  367. ''' get a specified key'''
  368. try:
  369. entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
  370. except KeyError:
  371. entry = None
  372. return entry
  373. def pop(self, path, key_or_item):
  374. ''' remove a key, value pair from a dict or an item for a list'''
  375. try:
  376. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  377. except KeyError:
  378. entry = None
  379. if entry is None:
  380. return (False, self.yaml_dict)
  381. if isinstance(entry, dict):
  382. # AUDIT:maybe-no-member makes sense due to fuzzy types
  383. # pylint: disable=maybe-no-member
  384. if key_or_item in entry:
  385. entry.pop(key_or_item)
  386. return (True, self.yaml_dict)
  387. return (False, self.yaml_dict)
  388. elif isinstance(entry, list):
  389. # AUDIT:maybe-no-member makes sense due to fuzzy types
  390. # pylint: disable=maybe-no-member
  391. ind = None
  392. try:
  393. ind = entry.index(key_or_item)
  394. except ValueError:
  395. return (False, self.yaml_dict)
  396. entry.pop(ind)
  397. return (True, self.yaml_dict)
  398. return (False, self.yaml_dict)
  399. def delete(self, path, index=None, value=None):
  400. ''' remove path from a dict'''
  401. try:
  402. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  403. except KeyError:
  404. entry = None
  405. if entry is None:
  406. return (False, self.yaml_dict)
  407. result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
  408. if not result:
  409. return (False, self.yaml_dict)
  410. return (True, self.yaml_dict)
  411. def exists(self, path, value):
  412. ''' check if value exists at path'''
  413. try:
  414. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  415. except KeyError:
  416. entry = None
  417. if isinstance(entry, list):
  418. if value in entry:
  419. return True
  420. return False
  421. elif isinstance(entry, dict):
  422. if isinstance(value, dict):
  423. rval = False
  424. for key, val in value.items():
  425. if entry[key] != val:
  426. rval = False
  427. break
  428. else:
  429. rval = True
  430. return rval
  431. return value in entry
  432. return entry == value
  433. def append(self, path, value):
  434. '''append value to a list'''
  435. try:
  436. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  437. except KeyError:
  438. entry = None
  439. if entry is None:
  440. self.put(path, [])
  441. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  442. if not isinstance(entry, list):
  443. return (False, self.yaml_dict)
  444. # AUDIT:maybe-no-member makes sense due to loading data from
  445. # a serialized format.
  446. # pylint: disable=maybe-no-member
  447. entry.append(value)
  448. return (True, self.yaml_dict)
  449. # pylint: disable=too-many-arguments
  450. def update(self, path, value, index=None, curr_value=None):
  451. ''' put path, value into a dict '''
  452. try:
  453. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  454. except KeyError:
  455. entry = None
  456. if isinstance(entry, dict):
  457. # AUDIT:maybe-no-member makes sense due to fuzzy types
  458. # pylint: disable=maybe-no-member
  459. if not isinstance(value, dict):
  460. raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
  461. 'value=[{}] type=[{}]'.format(value, type(value)))
  462. entry.update(value)
  463. return (True, self.yaml_dict)
  464. elif isinstance(entry, list):
  465. # AUDIT:maybe-no-member makes sense due to fuzzy types
  466. # pylint: disable=maybe-no-member
  467. ind = None
  468. if curr_value:
  469. try:
  470. ind = entry.index(curr_value)
  471. except ValueError:
  472. return (False, self.yaml_dict)
  473. elif index is not None:
  474. ind = index
  475. if ind is not None and entry[ind] != value:
  476. entry[ind] = value
  477. return (True, self.yaml_dict)
  478. # see if it exists in the list
  479. try:
  480. ind = entry.index(value)
  481. except ValueError:
  482. # doesn't exist, append it
  483. entry.append(value)
  484. return (True, self.yaml_dict)
  485. # already exists, return
  486. if ind is not None:
  487. return (False, self.yaml_dict)
  488. return (False, self.yaml_dict)
  489. def put(self, path, value):
  490. ''' put path, value into a dict '''
  491. try:
  492. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  493. except KeyError:
  494. entry = None
  495. if entry == value:
  496. return (False, self.yaml_dict)
  497. # deepcopy didn't work
  498. # Try to use ruamel.yaml and fallback to pyyaml
  499. try:
  500. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  501. default_flow_style=False),
  502. yaml.RoundTripLoader)
  503. except AttributeError:
  504. tmp_copy = copy.deepcopy(self.yaml_dict)
  505. # set the format attributes if available
  506. try:
  507. tmp_copy.fa.set_block_style()
  508. except AttributeError:
  509. pass
  510. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  511. if result is None:
  512. return (False, self.yaml_dict)
  513. # When path equals "" it is a special case.
  514. # "" refers to the root of the document
  515. # Only update the root path (entire document) when its a list or dict
  516. if path == '':
  517. if isinstance(result, list) or isinstance(result, dict):
  518. self.yaml_dict = result
  519. return (True, self.yaml_dict)
  520. return (False, self.yaml_dict)
  521. self.yaml_dict = tmp_copy
  522. return (True, self.yaml_dict)
  523. def create(self, path, value):
  524. ''' create a yaml file '''
  525. if not self.file_exists():
  526. # deepcopy didn't work
  527. # Try to use ruamel.yaml and fallback to pyyaml
  528. try:
  529. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  530. default_flow_style=False),
  531. yaml.RoundTripLoader)
  532. except AttributeError:
  533. tmp_copy = copy.deepcopy(self.yaml_dict)
  534. # set the format attributes if available
  535. try:
  536. tmp_copy.fa.set_block_style()
  537. except AttributeError:
  538. pass
  539. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  540. if result is not None:
  541. self.yaml_dict = tmp_copy
  542. return (True, self.yaml_dict)
  543. return (False, self.yaml_dict)
  544. @staticmethod
  545. def get_curr_value(invalue, val_type):
  546. '''return the current value'''
  547. if invalue is None:
  548. return None
  549. curr_value = invalue
  550. if val_type == 'yaml':
  551. try:
  552. # AUDIT:maybe-no-member makes sense due to different yaml libraries
  553. # pylint: disable=maybe-no-member
  554. curr_value = yaml.safe_load(invalue, Loader=yaml.RoundTripLoader)
  555. except AttributeError:
  556. curr_value = yaml.safe_load(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. separator=params['separator'])
  615. state = params['state']
  616. if params['src']:
  617. rval = yamlfile.load()
  618. if yamlfile.yaml_dict is None and state != 'present':
  619. return {'failed': True,
  620. 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
  621. 'file exists, that it is has correct permissions, and is valid yaml.'}
  622. if state == 'list':
  623. if params['content']:
  624. content = Yedit.parse_value(params['content'], params['content_type'])
  625. yamlfile.yaml_dict = content
  626. if params['key']:
  627. rval = yamlfile.get(params['key'])
  628. return {'changed': False, 'result': rval, 'state': state}
  629. elif state == 'absent':
  630. if params['content']:
  631. content = Yedit.parse_value(params['content'], params['content_type'])
  632. yamlfile.yaml_dict = content
  633. if params['update']:
  634. rval = yamlfile.pop(params['key'], params['value'])
  635. else:
  636. rval = yamlfile.delete(params['key'], params['index'], params['value'])
  637. if rval[0] and params['src']:
  638. yamlfile.write()
  639. return {'changed': rval[0], 'result': rval[1], 'state': state}
  640. elif state == 'present':
  641. # check if content is different than what is in the file
  642. if params['content']:
  643. content = Yedit.parse_value(params['content'], params['content_type'])
  644. # We had no edits to make and the contents are the same
  645. if yamlfile.yaml_dict == content and \
  646. params['value'] is None:
  647. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  648. yamlfile.yaml_dict = content
  649. # If we were passed a key, value then
  650. # we enapsulate it in a list and process it
  651. # Key, Value passed to the module : Converted to Edits list #
  652. edits = []
  653. _edit = {}
  654. if params['value'] is not None:
  655. _edit['value'] = params['value']
  656. _edit['value_type'] = params['value_type']
  657. _edit['key'] = params['key']
  658. if params['update']:
  659. _edit['action'] = 'update'
  660. _edit['curr_value'] = params['curr_value']
  661. _edit['curr_value_format'] = params['curr_value_format']
  662. _edit['index'] = params['index']
  663. elif params['append']:
  664. _edit['action'] = 'append'
  665. edits.append(_edit)
  666. elif params['edits'] is not None:
  667. edits = params['edits']
  668. if edits:
  669. results = Yedit.process_edits(edits, yamlfile)
  670. # if there were changes and a src provided to us we need to write
  671. if results['changed'] and params['src']:
  672. yamlfile.write()
  673. return {'changed': results['changed'], 'result': results['results'], 'state': state}
  674. # no edits to make
  675. if params['src']:
  676. # pylint: disable=redefined-variable-type
  677. rval = yamlfile.write()
  678. return {'changed': rval[0],
  679. 'result': rval[1],
  680. 'state': state}
  681. # We were passed content but no src, key or value, or edits. Return contents in memory
  682. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  683. return {'failed': True, 'msg': 'Unkown state passed'}
  684. # -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
  685. # -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
  686. # pylint: disable=too-many-lines
  687. # noqa: E301,E302,E303,T001
  688. class OpenShiftCLIError(Exception):
  689. '''Exception class for openshiftcli'''
  690. pass
  691. ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
  692. def locate_oc_binary():
  693. ''' Find and return oc binary file '''
  694. # https://github.com/openshift/openshift-ansible/issues/3410
  695. # oc can be in /usr/local/bin in some cases, but that may not
  696. # be in $PATH due to ansible/sudo
  697. paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
  698. oc_binary = 'oc'
  699. # Use shutil.which if it is available, otherwise fallback to a naive path search
  700. try:
  701. which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
  702. if which_result is not None:
  703. oc_binary = which_result
  704. except AttributeError:
  705. for path in paths:
  706. if os.path.exists(os.path.join(path, oc_binary)):
  707. oc_binary = os.path.join(path, oc_binary)
  708. break
  709. return oc_binary
  710. # pylint: disable=too-few-public-methods
  711. class OpenShiftCLI(object):
  712. ''' Class to wrap the command line tools '''
  713. def __init__(self,
  714. namespace,
  715. kubeconfig='/etc/origin/master/admin.kubeconfig',
  716. verbose=False,
  717. all_namespaces=False):
  718. ''' Constructor for OpenshiftCLI '''
  719. self.namespace = namespace
  720. self.verbose = verbose
  721. self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
  722. self.all_namespaces = all_namespaces
  723. self.oc_binary = locate_oc_binary()
  724. # Pylint allows only 5 arguments to be passed.
  725. # pylint: disable=too-many-arguments
  726. def _replace_content(self, resource, rname, content, edits=None, force=False, sep='.'):
  727. ''' replace the current object with the content '''
  728. res = self._get(resource, rname)
  729. if not res['results']:
  730. return res
  731. fname = Utils.create_tmpfile(rname + '-')
  732. yed = Yedit(fname, res['results'][0], separator=sep)
  733. updated = False
  734. if content is not None:
  735. changes = []
  736. for key, value in content.items():
  737. changes.append(yed.put(key, value))
  738. if any([change[0] for change in changes]):
  739. updated = True
  740. elif edits is not None:
  741. results = Yedit.process_edits(edits, yed)
  742. if results['changed']:
  743. updated = True
  744. if updated:
  745. yed.write()
  746. atexit.register(Utils.cleanup, [fname])
  747. return self._replace(fname, force)
  748. return {'returncode': 0, 'updated': False}
  749. def _replace(self, fname, force=False):
  750. '''replace the current object with oc replace'''
  751. # We are removing the 'resourceVersion' to handle
  752. # a race condition when modifying oc objects
  753. yed = Yedit(fname)
  754. results = yed.delete('metadata.resourceVersion')
  755. if results[0]:
  756. yed.write()
  757. cmd = ['replace', '-f', fname]
  758. if force:
  759. cmd.append('--force')
  760. return self.openshift_cmd(cmd)
  761. def _create_from_content(self, rname, content):
  762. '''create a temporary file and then call oc create on it'''
  763. fname = Utils.create_tmpfile(rname + '-')
  764. yed = Yedit(fname, content=content)
  765. yed.write()
  766. atexit.register(Utils.cleanup, [fname])
  767. return self._create(fname)
  768. def _create(self, fname):
  769. '''call oc create on a filename'''
  770. return self.openshift_cmd(['create', '-f', fname])
  771. def _delete(self, resource, name=None, selector=None):
  772. '''call oc delete on a resource'''
  773. cmd = ['delete', resource]
  774. if selector is not None:
  775. cmd.append('--selector={}'.format(selector))
  776. elif name is not None:
  777. cmd.append(name)
  778. else:
  779. raise OpenShiftCLIError('Either name or selector is required when calling delete.')
  780. return self.openshift_cmd(cmd)
  781. def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
  782. '''process a template
  783. template_name: the name of the template to process
  784. create: whether to send to oc create after processing
  785. params: the parameters for the template
  786. template_data: the incoming template's data; instead of a file
  787. '''
  788. cmd = ['process']
  789. if template_data:
  790. cmd.extend(['-f', '-'])
  791. else:
  792. cmd.append(template_name)
  793. if params:
  794. param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
  795. cmd.append('-v')
  796. cmd.extend(param_str)
  797. results = self.openshift_cmd(cmd, output=True, input_data=template_data)
  798. if results['returncode'] != 0 or not create:
  799. return results
  800. fname = Utils.create_tmpfile(template_name + '-')
  801. yed = Yedit(fname, results['results'])
  802. yed.write()
  803. atexit.register(Utils.cleanup, [fname])
  804. return self.openshift_cmd(['create', '-f', fname])
  805. def _get(self, resource, name=None, selector=None, field_selector=None):
  806. '''return a resource by name '''
  807. cmd = ['get', resource]
  808. if selector is not None:
  809. cmd.append('--selector={}'.format(selector))
  810. if field_selector is not None:
  811. cmd.append('--field-selector={}'.format(field_selector))
  812. # Name cannot be used with selector or field_selector.
  813. if selector is None and field_selector is None and name is not None:
  814. cmd.append(name)
  815. cmd.extend(['-o', 'json'])
  816. rval = self.openshift_cmd(cmd, output=True)
  817. # Ensure results are retuned in an array
  818. if 'items' in rval:
  819. rval['results'] = rval['items']
  820. elif not isinstance(rval['results'], list):
  821. rval['results'] = [rval['results']]
  822. return rval
  823. def _schedulable(self, node=None, selector=None, schedulable=True):
  824. ''' perform oadm manage-node scheduable '''
  825. cmd = ['manage-node']
  826. if node:
  827. cmd.extend(node)
  828. else:
  829. cmd.append('--selector={}'.format(selector))
  830. cmd.append('--schedulable={}'.format(schedulable))
  831. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
  832. def _list_pods(self, node=None, selector=None, pod_selector=None):
  833. ''' perform oadm list pods
  834. node: the node in which to list pods
  835. selector: the label selector filter if provided
  836. pod_selector: the pod selector filter if provided
  837. '''
  838. cmd = ['manage-node']
  839. if node:
  840. cmd.extend(node)
  841. else:
  842. cmd.append('--selector={}'.format(selector))
  843. if pod_selector:
  844. cmd.append('--pod-selector={}'.format(pod_selector))
  845. cmd.extend(['--list-pods', '-o', 'json'])
  846. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  847. # pylint: disable=too-many-arguments
  848. def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
  849. ''' perform oadm manage-node evacuate '''
  850. cmd = ['manage-node']
  851. if node:
  852. cmd.extend(node)
  853. else:
  854. cmd.append('--selector={}'.format(selector))
  855. if dry_run:
  856. cmd.append('--dry-run')
  857. if pod_selector:
  858. cmd.append('--pod-selector={}'.format(pod_selector))
  859. if grace_period:
  860. cmd.append('--grace-period={}'.format(int(grace_period)))
  861. if force:
  862. cmd.append('--force')
  863. cmd.append('--evacuate')
  864. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  865. def _version(self):
  866. ''' return the openshift version'''
  867. return self.openshift_cmd(['version'], output=True, output_type='raw')
  868. def _import_image(self, url=None, name=None, tag=None):
  869. ''' perform image import '''
  870. cmd = ['import-image']
  871. image = '{0}'.format(name)
  872. if tag:
  873. image += ':{0}'.format(tag)
  874. cmd.append(image)
  875. if url:
  876. cmd.append('--from={0}/{1}'.format(url, image))
  877. cmd.append('-n{0}'.format(self.namespace))
  878. cmd.append('--confirm')
  879. return self.openshift_cmd(cmd)
  880. def _run(self, cmds, input_data):
  881. ''' Actually executes the command. This makes mocking easier. '''
  882. curr_env = os.environ.copy()
  883. curr_env.update({'KUBECONFIG': self.kubeconfig})
  884. proc = subprocess.Popen(cmds,
  885. stdin=subprocess.PIPE,
  886. stdout=subprocess.PIPE,
  887. stderr=subprocess.PIPE,
  888. env=curr_env)
  889. stdout, stderr = proc.communicate(input_data)
  890. return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
  891. # pylint: disable=too-many-arguments,too-many-branches
  892. def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
  893. '''Base command for oc '''
  894. cmds = [self.oc_binary]
  895. if oadm:
  896. cmds.append('adm')
  897. cmds.extend(cmd)
  898. if self.all_namespaces:
  899. cmds.extend(['--all-namespaces'])
  900. elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
  901. cmds.extend(['-n', self.namespace])
  902. if self.verbose:
  903. print(' '.join(cmds))
  904. try:
  905. returncode, stdout, stderr = self._run(cmds, input_data)
  906. except OSError as ex:
  907. returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
  908. rval = {"returncode": returncode,
  909. "cmd": ' '.join(cmds)}
  910. if output_type == 'json':
  911. rval['results'] = {}
  912. if output and stdout:
  913. try:
  914. rval['results'] = json.loads(stdout)
  915. except ValueError as verr:
  916. if "No JSON object could be decoded" in verr.args:
  917. rval['err'] = verr.args
  918. elif output_type == 'raw':
  919. rval['results'] = stdout if output else ''
  920. if self.verbose:
  921. print("STDOUT: {0}".format(stdout))
  922. print("STDERR: {0}".format(stderr))
  923. if 'err' in rval or returncode != 0:
  924. rval.update({"stderr": stderr,
  925. "stdout": stdout})
  926. return rval
  927. class Utils(object): # pragma: no cover
  928. ''' utilities for openshiftcli modules '''
  929. @staticmethod
  930. def _write(filename, contents):
  931. ''' Actually write the file contents to disk. This helps with mocking. '''
  932. with open(filename, 'w') as sfd:
  933. sfd.write(str(contents))
  934. @staticmethod
  935. def create_tmp_file_from_contents(rname, data, ftype='yaml'):
  936. ''' create a file in tmp with name and contents'''
  937. tmp = Utils.create_tmpfile(prefix=rname)
  938. if ftype == 'yaml':
  939. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  940. # pylint: disable=no-member
  941. if hasattr(yaml, 'RoundTripDumper'):
  942. Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
  943. else:
  944. Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
  945. elif ftype == 'json':
  946. Utils._write(tmp, json.dumps(data))
  947. else:
  948. Utils._write(tmp, data)
  949. # Register cleanup when module is done
  950. atexit.register(Utils.cleanup, [tmp])
  951. return tmp
  952. @staticmethod
  953. def create_tmpfile_copy(inc_file):
  954. '''create a temporary copy of a file'''
  955. tmpfile = Utils.create_tmpfile('lib_openshift-')
  956. Utils._write(tmpfile, open(inc_file).read())
  957. # Cleanup the tmpfile
  958. atexit.register(Utils.cleanup, [tmpfile])
  959. return tmpfile
  960. @staticmethod
  961. def create_tmpfile(prefix='tmp'):
  962. ''' Generates and returns a temporary file name '''
  963. with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
  964. return tmp.name
  965. @staticmethod
  966. def create_tmp_files_from_contents(content, content_type=None):
  967. '''Turn an array of dict: filename, content into a files array'''
  968. if not isinstance(content, list):
  969. content = [content]
  970. files = []
  971. for item in content:
  972. path = Utils.create_tmp_file_from_contents(item['path'] + '-',
  973. item['data'],
  974. ftype=content_type)
  975. files.append({'name': os.path.basename(item['path']),
  976. 'path': path})
  977. return files
  978. @staticmethod
  979. def cleanup(files):
  980. '''Clean up on exit '''
  981. for sfile in files:
  982. if os.path.exists(sfile):
  983. if os.path.isdir(sfile):
  984. shutil.rmtree(sfile)
  985. elif os.path.isfile(sfile):
  986. os.remove(sfile)
  987. @staticmethod
  988. def exists(results, _name):
  989. ''' Check to see if the results include the name '''
  990. if not results:
  991. return False
  992. if Utils.find_result(results, _name):
  993. return True
  994. return False
  995. @staticmethod
  996. def find_result(results, _name):
  997. ''' Find the specified result by name'''
  998. rval = None
  999. for result in results:
  1000. if 'metadata' in result and result['metadata']['name'] == _name:
  1001. rval = result
  1002. break
  1003. return rval
  1004. @staticmethod
  1005. def get_resource_file(sfile, sfile_type='yaml'):
  1006. ''' return the service file '''
  1007. contents = None
  1008. with open(sfile) as sfd:
  1009. contents = sfd.read()
  1010. if sfile_type == 'yaml':
  1011. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  1012. # pylint: disable=no-member
  1013. if hasattr(yaml, 'RoundTripLoader'):
  1014. contents = yaml.load(contents, yaml.RoundTripLoader)
  1015. else:
  1016. contents = yaml.safe_load(contents)
  1017. elif sfile_type == 'json':
  1018. contents = json.loads(contents)
  1019. return contents
  1020. @staticmethod
  1021. def filter_versions(stdout):
  1022. ''' filter the oc version output '''
  1023. version_dict = {}
  1024. version_search = ['oc', 'openshift', 'kubernetes']
  1025. for line in stdout.strip().split('\n'):
  1026. for term in version_search:
  1027. if not line:
  1028. continue
  1029. if line.startswith(term):
  1030. version_dict[term] = line.split()[-1]
  1031. # horrible hack to get openshift version in Openshift 3.2
  1032. # By default "oc version in 3.2 does not return an "openshift" version
  1033. if "openshift" not in version_dict:
  1034. version_dict["openshift"] = version_dict["oc"]
  1035. return version_dict
  1036. @staticmethod
  1037. def add_custom_versions(versions):
  1038. ''' create custom versions strings '''
  1039. versions_dict = {}
  1040. for tech, version in versions.items():
  1041. # clean up "-" from version
  1042. if "-" in version:
  1043. version = version.split("-")[0]
  1044. if version.startswith('v'):
  1045. versions_dict[tech + '_numeric'] = version[1:].split('+')[0]
  1046. # "v3.3.0.33" is what we have, we want "3.3"
  1047. versions_dict[tech + '_short'] = version[1:4]
  1048. return versions_dict
  1049. @staticmethod
  1050. def openshift_installed():
  1051. ''' check if openshift is installed '''
  1052. import rpm
  1053. transaction_set = rpm.TransactionSet()
  1054. rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
  1055. return rpmquery.count() > 0
  1056. # Disabling too-many-branches. This is a yaml dictionary comparison function
  1057. # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
  1058. @staticmethod
  1059. def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
  1060. ''' Given a user defined definition, compare it with the results given back by our query. '''
  1061. # Currently these values are autogenerated and we do not need to check them
  1062. skip = ['metadata', 'status']
  1063. if skip_keys:
  1064. skip.extend(skip_keys)
  1065. for key, value in result_def.items():
  1066. if key in skip:
  1067. continue
  1068. # Both are lists
  1069. if isinstance(value, list):
  1070. if key not in user_def:
  1071. if debug:
  1072. print('User data does not have key [%s]' % key)
  1073. print('User data: %s' % user_def)
  1074. return False
  1075. if not isinstance(user_def[key], list):
  1076. if debug:
  1077. print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
  1078. return False
  1079. if len(user_def[key]) != len(value):
  1080. if debug:
  1081. print("List lengths are not equal.")
  1082. print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
  1083. print("user_def: %s" % user_def[key])
  1084. print("value: %s" % value)
  1085. return False
  1086. for values in zip(user_def[key], value):
  1087. if isinstance(values[0], dict) and isinstance(values[1], dict):
  1088. if debug:
  1089. print('sending list - list')
  1090. print(type(values[0]))
  1091. print(type(values[1]))
  1092. result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
  1093. if not result:
  1094. print('list compare returned false')
  1095. return False
  1096. elif value != user_def[key]:
  1097. if debug:
  1098. print('value should be identical')
  1099. print(user_def[key])
  1100. print(value)
  1101. return False
  1102. # recurse on a dictionary
  1103. elif isinstance(value, dict):
  1104. if key not in user_def:
  1105. if debug:
  1106. print("user_def does not have key [%s]" % key)
  1107. return False
  1108. if not isinstance(user_def[key], dict):
  1109. if debug:
  1110. print("dict returned false: not instance of dict")
  1111. return False
  1112. # before passing ensure keys match
  1113. api_values = set(value.keys()) - set(skip)
  1114. user_values = set(user_def[key].keys()) - set(skip)
  1115. if api_values != user_values:
  1116. if debug:
  1117. print("keys are not equal in dict")
  1118. print(user_values)
  1119. print(api_values)
  1120. return False
  1121. result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
  1122. if not result:
  1123. if debug:
  1124. print("dict returned false")
  1125. print(result)
  1126. return False
  1127. # Verify each key, value pair is the same
  1128. else:
  1129. if key not in user_def or value != user_def[key]:
  1130. if debug:
  1131. print("value not equal; user_def does not have key")
  1132. print(key)
  1133. print(value)
  1134. if key in user_def:
  1135. print(user_def[key])
  1136. return False
  1137. if debug:
  1138. print('returning true')
  1139. return True
  1140. class OpenShiftCLIConfig(object):
  1141. '''Generic Config'''
  1142. def __init__(self, rname, namespace, kubeconfig, options):
  1143. self.kubeconfig = kubeconfig
  1144. self.name = rname
  1145. self.namespace = namespace
  1146. self._options = options
  1147. @property
  1148. def config_options(self):
  1149. ''' return config options '''
  1150. return self._options
  1151. def to_option_list(self, ascommalist=''):
  1152. '''return all options as a string
  1153. if ascommalist is set to the name of a key, and
  1154. the value of that key is a dict, format the dict
  1155. as a list of comma delimited key=value pairs'''
  1156. return self.stringify(ascommalist)
  1157. def stringify(self, ascommalist=''):
  1158. ''' return the options hash as cli params in a string
  1159. if ascommalist is set to the name of a key, and
  1160. the value of that key is a dict, format the dict
  1161. as a list of comma delimited key=value pairs '''
  1162. rval = []
  1163. for key in sorted(self.config_options.keys()):
  1164. data = self.config_options[key]
  1165. if data['include'] \
  1166. and (data['value'] is not None or isinstance(data['value'], int)):
  1167. if key == ascommalist:
  1168. val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
  1169. else:
  1170. val = data['value']
  1171. rval.append('--{}={}'.format(key.replace('_', '-'), val))
  1172. return rval
  1173. # -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
  1174. # -*- -*- -*- Begin included fragment: lib/deploymentconfig.py -*- -*- -*-
  1175. # pylint: disable=too-many-public-methods
  1176. class DeploymentConfig(Yedit):
  1177. ''' Class to model an openshift DeploymentConfig'''
  1178. default_deployment_config = '''
  1179. apiVersion: v1
  1180. kind: DeploymentConfig
  1181. metadata:
  1182. name: default_dc
  1183. namespace: default
  1184. spec:
  1185. replicas: 0
  1186. selector:
  1187. default_dc: default_dc
  1188. strategy:
  1189. resources: {}
  1190. rollingParams:
  1191. intervalSeconds: 1
  1192. maxSurge: 0
  1193. maxUnavailable: 25%
  1194. timeoutSeconds: 600
  1195. updatePercent: -25
  1196. updatePeriodSeconds: 1
  1197. type: Rolling
  1198. template:
  1199. metadata:
  1200. spec:
  1201. containers:
  1202. - env:
  1203. - name: default
  1204. value: default
  1205. image: default
  1206. imagePullPolicy: IfNotPresent
  1207. name: default_dc
  1208. ports:
  1209. - containerPort: 8000
  1210. hostPort: 8000
  1211. protocol: TCP
  1212. name: default_port
  1213. resources: {}
  1214. terminationMessagePath: /dev/termination-log
  1215. dnsPolicy: ClusterFirst
  1216. hostNetwork: true
  1217. nodeSelector:
  1218. type: compute
  1219. restartPolicy: Always
  1220. securityContext: {}
  1221. serviceAccount: default
  1222. serviceAccountName: default
  1223. terminationGracePeriodSeconds: 30
  1224. triggers:
  1225. - type: ConfigChange
  1226. '''
  1227. replicas_path = "spec.replicas"
  1228. env_path = "spec.template.spec.containers[0].env"
  1229. volumes_path = "spec.template.spec.volumes"
  1230. container_path = "spec.template.spec.containers"
  1231. volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
  1232. def __init__(self, content=None):
  1233. ''' Constructor for deploymentconfig '''
  1234. if not content:
  1235. content = DeploymentConfig.default_deployment_config
  1236. super(DeploymentConfig, self).__init__(content=content)
  1237. def add_env_value(self, key, value):
  1238. ''' add key, value pair to env array '''
  1239. rval = False
  1240. env = self.get_env_vars()
  1241. if env:
  1242. env.append({'name': key, 'value': value})
  1243. rval = True
  1244. else:
  1245. result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value})
  1246. rval = result[0]
  1247. return rval
  1248. def exists_env_value(self, key, value):
  1249. ''' return whether a key, value pair exists '''
  1250. results = self.get_env_vars()
  1251. if not results:
  1252. return False
  1253. for result in results:
  1254. if result['name'] == key and result['value'] == value:
  1255. return True
  1256. return False
  1257. def exists_env_key(self, key):
  1258. ''' return whether a key, value pair exists '''
  1259. results = self.get_env_vars()
  1260. if not results:
  1261. return False
  1262. for result in results:
  1263. if result['name'] == key:
  1264. return True
  1265. return False
  1266. def get_env_var(self, key):
  1267. '''return a environment variables '''
  1268. results = self.get(DeploymentConfig.env_path) or []
  1269. if not results:
  1270. return None
  1271. for env_var in results:
  1272. if env_var['name'] == key:
  1273. return env_var
  1274. return None
  1275. def get_env_vars(self):
  1276. '''return a environment variables '''
  1277. return self.get(DeploymentConfig.env_path) or []
  1278. def delete_env_var(self, keys):
  1279. '''delete a list of keys '''
  1280. if not isinstance(keys, list):
  1281. keys = [keys]
  1282. env_vars_array = self.get_env_vars()
  1283. modified = False
  1284. idx = None
  1285. for key in keys:
  1286. for env_idx, env_var in enumerate(env_vars_array):
  1287. if env_var['name'] == key:
  1288. idx = env_idx
  1289. break
  1290. if idx:
  1291. modified = True
  1292. del env_vars_array[idx]
  1293. if modified:
  1294. return True
  1295. return False
  1296. def update_env_var(self, key, value):
  1297. '''place an env in the env var list'''
  1298. env_vars_array = self.get_env_vars()
  1299. idx = None
  1300. for env_idx, env_var in enumerate(env_vars_array):
  1301. if env_var['name'] == key:
  1302. idx = env_idx
  1303. break
  1304. if idx:
  1305. env_vars_array[idx]['value'] = value
  1306. else:
  1307. self.add_env_value(key, value)
  1308. return True
  1309. def exists_volume_mount(self, volume_mount):
  1310. ''' return whether a volume mount exists '''
  1311. exist_volume_mounts = self.get_volume_mounts()
  1312. if not exist_volume_mounts:
  1313. return False
  1314. volume_mount_found = False
  1315. for exist_volume_mount in exist_volume_mounts:
  1316. if exist_volume_mount['name'] == volume_mount['name']:
  1317. volume_mount_found = True
  1318. break
  1319. return volume_mount_found
  1320. def exists_volume(self, volume):
  1321. ''' return whether a volume exists '''
  1322. exist_volumes = self.get_volumes()
  1323. volume_found = False
  1324. for exist_volume in exist_volumes:
  1325. if exist_volume['name'] == volume['name']:
  1326. volume_found = True
  1327. break
  1328. return volume_found
  1329. def find_volume_by_name(self, volume, mounts=False):
  1330. ''' return the index of a volume '''
  1331. volumes = []
  1332. if mounts:
  1333. volumes = self.get_volume_mounts()
  1334. else:
  1335. volumes = self.get_volumes()
  1336. for exist_volume in volumes:
  1337. if exist_volume['name'] == volume['name']:
  1338. return exist_volume
  1339. return None
  1340. def get_replicas(self):
  1341. ''' return replicas setting '''
  1342. return self.get(DeploymentConfig.replicas_path)
  1343. def get_volume_mounts(self):
  1344. '''return volume mount information '''
  1345. return self.get_volumes(mounts=True)
  1346. def get_volumes(self, mounts=False):
  1347. '''return volume mount information '''
  1348. if mounts:
  1349. return self.get(DeploymentConfig.volume_mounts_path) or []
  1350. return self.get(DeploymentConfig.volumes_path) or []
  1351. def delete_volume_by_name(self, volume):
  1352. '''delete a volume '''
  1353. modified = False
  1354. exist_volume_mounts = self.get_volume_mounts()
  1355. exist_volumes = self.get_volumes()
  1356. del_idx = None
  1357. for idx, exist_volume in enumerate(exist_volumes):
  1358. if 'name' in exist_volume and exist_volume['name'] == volume['name']:
  1359. del_idx = idx
  1360. break
  1361. if del_idx != None:
  1362. del exist_volumes[del_idx]
  1363. modified = True
  1364. del_idx = None
  1365. for idx, exist_volume_mount in enumerate(exist_volume_mounts):
  1366. if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']:
  1367. del_idx = idx
  1368. break
  1369. if del_idx != None:
  1370. del exist_volume_mounts[idx]
  1371. modified = True
  1372. return modified
  1373. def add_volume_mount(self, volume_mount):
  1374. ''' add a volume or volume mount to the proper location '''
  1375. exist_volume_mounts = self.get_volume_mounts()
  1376. if not exist_volume_mounts and volume_mount:
  1377. self.put(DeploymentConfig.volume_mounts_path, [volume_mount])
  1378. else:
  1379. exist_volume_mounts.append(volume_mount)
  1380. def add_volume(self, volume):
  1381. ''' add a volume or volume mount to the proper location '''
  1382. exist_volumes = self.get_volumes()
  1383. if not volume:
  1384. return
  1385. if not exist_volumes:
  1386. self.put(DeploymentConfig.volumes_path, [volume])
  1387. else:
  1388. exist_volumes.append(volume)
  1389. def update_replicas(self, replicas):
  1390. ''' update replicas value '''
  1391. self.put(DeploymentConfig.replicas_path, replicas)
  1392. def update_volume(self, volume):
  1393. '''place an env in the env var list'''
  1394. exist_volumes = self.get_volumes()
  1395. if not volume:
  1396. return False
  1397. # update the volume
  1398. update_idx = None
  1399. for idx, exist_vol in enumerate(exist_volumes):
  1400. if exist_vol['name'] == volume['name']:
  1401. update_idx = idx
  1402. break
  1403. if update_idx != None:
  1404. exist_volumes[update_idx] = volume
  1405. else:
  1406. self.add_volume(volume)
  1407. return True
  1408. def update_volume_mount(self, volume_mount):
  1409. '''place an env in the env var list'''
  1410. modified = False
  1411. exist_volume_mounts = self.get_volume_mounts()
  1412. if not volume_mount:
  1413. return False
  1414. # update the volume mount
  1415. for exist_vol_mount in exist_volume_mounts:
  1416. if exist_vol_mount['name'] == volume_mount['name']:
  1417. if 'mountPath' in exist_vol_mount and \
  1418. str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']):
  1419. exist_vol_mount['mountPath'] = volume_mount['mountPath']
  1420. modified = True
  1421. break
  1422. if not modified:
  1423. self.add_volume_mount(volume_mount)
  1424. modified = True
  1425. return modified
  1426. def needs_update_volume(self, volume, volume_mount):
  1427. ''' verify a volume update is needed '''
  1428. exist_volume = self.find_volume_by_name(volume)
  1429. exist_volume_mount = self.find_volume_by_name(volume, mounts=True)
  1430. results = []
  1431. results.append(exist_volume['name'] == volume['name'])
  1432. if 'secret' in volume:
  1433. results.append('secret' in exist_volume)
  1434. results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName'])
  1435. results.append(exist_volume_mount['name'] == volume_mount['name'])
  1436. results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
  1437. elif 'emptyDir' in volume:
  1438. results.append(exist_volume_mount['name'] == volume['name'])
  1439. results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
  1440. elif 'persistentVolumeClaim' in volume:
  1441. pvc = 'persistentVolumeClaim'
  1442. results.append(pvc in exist_volume)
  1443. if results[-1]:
  1444. results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName'])
  1445. if 'claimSize' in volume[pvc]:
  1446. results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize'])
  1447. elif 'hostpath' in volume:
  1448. results.append('hostPath' in exist_volume)
  1449. results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath'])
  1450. return not all(results)
  1451. def needs_update_replicas(self, replicas):
  1452. ''' verify whether a replica update is needed '''
  1453. current_reps = self.get(DeploymentConfig.replicas_path)
  1454. return not current_reps == replicas
  1455. # -*- -*- -*- End included fragment: lib/deploymentconfig.py -*- -*- -*-
  1456. # -*- -*- -*- Begin included fragment: lib/replicationcontroller.py -*- -*- -*-
  1457. # pylint: disable=too-many-public-methods
  1458. class ReplicationController(DeploymentConfig):
  1459. ''' Class to model a replicationcontroller openshift object.
  1460. Currently we are modeled after a deployment config since they
  1461. are very similar. In the future, when the need arises we
  1462. will add functionality to this class.
  1463. '''
  1464. replicas_path = "spec.replicas"
  1465. env_path = "spec.template.spec.containers[0].env"
  1466. volumes_path = "spec.template.spec.volumes"
  1467. container_path = "spec.template.spec.containers"
  1468. volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
  1469. def __init__(self, content):
  1470. ''' Constructor for ReplicationController '''
  1471. super(ReplicationController, self).__init__(content=content)
  1472. # -*- -*- -*- End included fragment: lib/replicationcontroller.py -*- -*- -*-
  1473. # -*- -*- -*- Begin included fragment: class/oc_scale.py -*- -*- -*-
  1474. # pylint: disable=too-many-instance-attributes
  1475. class OCScale(OpenShiftCLI):
  1476. ''' Class to wrap the oc command line tools '''
  1477. # pylint allows 5
  1478. # pylint: disable=too-many-arguments
  1479. def __init__(self,
  1480. resource_name,
  1481. namespace,
  1482. replicas,
  1483. kind,
  1484. kubeconfig='/etc/origin/master/admin.kubeconfig',
  1485. verbose=False):
  1486. ''' Constructor for OCScale '''
  1487. super(OCScale, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
  1488. self.kind = kind
  1489. self.replicas = replicas
  1490. self.name = resource_name
  1491. self._resource = None
  1492. @property
  1493. def resource(self):
  1494. ''' property function for resource var '''
  1495. if not self._resource:
  1496. self.get()
  1497. return self._resource
  1498. @resource.setter
  1499. def resource(self, data):
  1500. ''' setter function for resource var '''
  1501. self._resource = data
  1502. def get(self):
  1503. '''return replicas information '''
  1504. vol = self._get(self.kind, self.name)
  1505. if vol['returncode'] == 0:
  1506. if self.kind == 'dc':
  1507. # The resource returned from a query could be an rc or dc.
  1508. # pylint: disable=redefined-variable-type
  1509. self.resource = DeploymentConfig(content=vol['results'][0])
  1510. vol['results'] = [self.resource.get_replicas()]
  1511. if self.kind == 'rc':
  1512. # The resource returned from a query could be an rc or dc.
  1513. # pylint: disable=redefined-variable-type
  1514. self.resource = ReplicationController(content=vol['results'][0])
  1515. vol['results'] = [self.resource.get_replicas()]
  1516. return vol
  1517. def put(self):
  1518. '''update replicas into dc '''
  1519. self.resource.update_replicas(self.replicas)
  1520. return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
  1521. def needs_update(self):
  1522. ''' verify whether an update is needed '''
  1523. return self.resource.needs_update_replicas(self.replicas)
  1524. # pylint: disable=too-many-return-statements
  1525. @staticmethod
  1526. def run_ansible(params, check_mode):
  1527. '''perform the idempotent ansible logic'''
  1528. oc_scale = OCScale(params['name'],
  1529. params['namespace'],
  1530. params['replicas'],
  1531. params['kind'],
  1532. params['kubeconfig'],
  1533. verbose=params['debug'])
  1534. state = params['state']
  1535. api_rval = oc_scale.get()
  1536. if api_rval['returncode'] != 0:
  1537. return {'failed': True, 'msg': api_rval}
  1538. #####
  1539. # Get
  1540. #####
  1541. if state == 'list':
  1542. return {'changed': False, 'result': api_rval['results'], 'state': 'list'} # noqa: E501
  1543. elif state == 'present':
  1544. ########
  1545. # Update
  1546. ########
  1547. if oc_scale.needs_update():
  1548. if check_mode:
  1549. return {'changed': True, 'result': 'CHECK_MODE: Would have updated.'} # noqa: E501
  1550. api_rval = oc_scale.put()
  1551. if api_rval['returncode'] != 0:
  1552. return {'failed': True, 'msg': api_rval}
  1553. # return the created object
  1554. api_rval = oc_scale.get()
  1555. if api_rval['returncode'] != 0:
  1556. return {'failed': True, 'msg': api_rval}
  1557. return {'changed': True, 'result': api_rval['results'], 'state': 'present'} # noqa: E501
  1558. return {'changed': False, 'result': api_rval['results'], 'state': 'present'} # noqa: E501
  1559. return {'failed': True, 'msg': 'Unknown state passed. [{}]'.format(state)}
  1560. # -*- -*- -*- End included fragment: class/oc_scale.py -*- -*- -*-
  1561. # -*- -*- -*- Begin included fragment: ansible/oc_scale.py -*- -*- -*-
  1562. def main():
  1563. '''
  1564. ansible oc module for scaling
  1565. '''
  1566. module = AnsibleModule(
  1567. argument_spec=dict(
  1568. kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
  1569. state=dict(default='present', type='str', choices=['present', 'list']),
  1570. debug=dict(default=False, type='bool'),
  1571. kind=dict(default='dc', choices=['dc', 'rc'], type='str'),
  1572. namespace=dict(default='default', type='str'),
  1573. replicas=dict(default=None, type='int'),
  1574. name=dict(default=None, type='str'),
  1575. ),
  1576. supports_check_mode=True,
  1577. )
  1578. rval = OCScale.run_ansible(module.params, module.check_mode)
  1579. if 'failed' in rval:
  1580. module.fail_json(**rval)
  1581. module.exit_json(**rval)
  1582. if __name__ == '__main__':
  1583. main()
  1584. # -*- -*- -*- End included fragment: ansible/oc_scale.py -*- -*- -*-