oc_scale.py 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975
  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,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/deploymentconfig.py -*- -*- -*-
  1177. # pylint: disable=too-many-public-methods
  1178. class DeploymentConfig(Yedit):
  1179. ''' Class to model an openshift DeploymentConfig'''
  1180. default_deployment_config = '''
  1181. apiVersion: v1
  1182. kind: DeploymentConfig
  1183. metadata:
  1184. name: default_dc
  1185. namespace: default
  1186. spec:
  1187. replicas: 0
  1188. selector:
  1189. default_dc: default_dc
  1190. strategy:
  1191. resources: {}
  1192. rollingParams:
  1193. intervalSeconds: 1
  1194. maxSurge: 0
  1195. maxUnavailable: 25%
  1196. timeoutSeconds: 600
  1197. updatePercent: -25
  1198. updatePeriodSeconds: 1
  1199. type: Rolling
  1200. template:
  1201. metadata:
  1202. spec:
  1203. containers:
  1204. - env:
  1205. - name: default
  1206. value: default
  1207. image: default
  1208. imagePullPolicy: IfNotPresent
  1209. name: default_dc
  1210. ports:
  1211. - containerPort: 8000
  1212. hostPort: 8000
  1213. protocol: TCP
  1214. name: default_port
  1215. resources: {}
  1216. terminationMessagePath: /dev/termination-log
  1217. dnsPolicy: ClusterFirst
  1218. hostNetwork: true
  1219. nodeSelector:
  1220. type: compute
  1221. restartPolicy: Always
  1222. securityContext: {}
  1223. serviceAccount: default
  1224. serviceAccountName: default
  1225. terminationGracePeriodSeconds: 30
  1226. triggers:
  1227. - type: ConfigChange
  1228. '''
  1229. replicas_path = "spec.replicas"
  1230. env_path = "spec.template.spec.containers[0].env"
  1231. volumes_path = "spec.template.spec.volumes"
  1232. container_path = "spec.template.spec.containers"
  1233. volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
  1234. def __init__(self, content=None):
  1235. ''' Constructor for deploymentconfig '''
  1236. if not content:
  1237. content = DeploymentConfig.default_deployment_config
  1238. super(DeploymentConfig, self).__init__(content=content)
  1239. def add_env_value(self, key, value):
  1240. ''' add key, value pair to env array '''
  1241. rval = False
  1242. env = self.get_env_vars()
  1243. if env:
  1244. env.append({'name': key, 'value': value})
  1245. rval = True
  1246. else:
  1247. result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value})
  1248. rval = result[0]
  1249. return rval
  1250. def exists_env_value(self, key, value):
  1251. ''' return whether a key, value pair exists '''
  1252. results = self.get_env_vars()
  1253. if not results:
  1254. return False
  1255. for result in results:
  1256. if result['name'] == key and result['value'] == value:
  1257. return True
  1258. return False
  1259. def exists_env_key(self, key):
  1260. ''' return whether a key, value pair exists '''
  1261. results = self.get_env_vars()
  1262. if not results:
  1263. return False
  1264. for result in results:
  1265. if result['name'] == key:
  1266. return True
  1267. return False
  1268. def get_env_var(self, key):
  1269. '''return a environment variables '''
  1270. results = self.get(DeploymentConfig.env_path) or []
  1271. if not results:
  1272. return None
  1273. for env_var in results:
  1274. if env_var['name'] == key:
  1275. return env_var
  1276. return None
  1277. def get_env_vars(self):
  1278. '''return a environment variables '''
  1279. return self.get(DeploymentConfig.env_path) or []
  1280. def delete_env_var(self, keys):
  1281. '''delete a list of keys '''
  1282. if not isinstance(keys, list):
  1283. keys = [keys]
  1284. env_vars_array = self.get_env_vars()
  1285. modified = False
  1286. idx = None
  1287. for key in keys:
  1288. for env_idx, env_var in enumerate(env_vars_array):
  1289. if env_var['name'] == key:
  1290. idx = env_idx
  1291. break
  1292. if idx:
  1293. modified = True
  1294. del env_vars_array[idx]
  1295. if modified:
  1296. return True
  1297. return False
  1298. def update_env_var(self, key, value):
  1299. '''place an env in the env var list'''
  1300. env_vars_array = self.get_env_vars()
  1301. idx = None
  1302. for env_idx, env_var in enumerate(env_vars_array):
  1303. if env_var['name'] == key:
  1304. idx = env_idx
  1305. break
  1306. if idx:
  1307. env_vars_array[idx]['value'] = value
  1308. else:
  1309. self.add_env_value(key, value)
  1310. return True
  1311. def exists_volume_mount(self, volume_mount):
  1312. ''' return whether a volume mount exists '''
  1313. exist_volume_mounts = self.get_volume_mounts()
  1314. if not exist_volume_mounts:
  1315. return False
  1316. volume_mount_found = False
  1317. for exist_volume_mount in exist_volume_mounts:
  1318. if exist_volume_mount['name'] == volume_mount['name']:
  1319. volume_mount_found = True
  1320. break
  1321. return volume_mount_found
  1322. def exists_volume(self, volume):
  1323. ''' return whether a volume exists '''
  1324. exist_volumes = self.get_volumes()
  1325. volume_found = False
  1326. for exist_volume in exist_volumes:
  1327. if exist_volume['name'] == volume['name']:
  1328. volume_found = True
  1329. break
  1330. return volume_found
  1331. def find_volume_by_name(self, volume, mounts=False):
  1332. ''' return the index of a volume '''
  1333. volumes = []
  1334. if mounts:
  1335. volumes = self.get_volume_mounts()
  1336. else:
  1337. volumes = self.get_volumes()
  1338. for exist_volume in volumes:
  1339. if exist_volume['name'] == volume['name']:
  1340. return exist_volume
  1341. return None
  1342. def get_replicas(self):
  1343. ''' return replicas setting '''
  1344. return self.get(DeploymentConfig.replicas_path)
  1345. def get_volume_mounts(self):
  1346. '''return volume mount information '''
  1347. return self.get_volumes(mounts=True)
  1348. def get_volumes(self, mounts=False):
  1349. '''return volume mount information '''
  1350. if mounts:
  1351. return self.get(DeploymentConfig.volume_mounts_path) or []
  1352. return self.get(DeploymentConfig.volumes_path) or []
  1353. def delete_volume_by_name(self, volume):
  1354. '''delete a volume '''
  1355. modified = False
  1356. exist_volume_mounts = self.get_volume_mounts()
  1357. exist_volumes = self.get_volumes()
  1358. del_idx = None
  1359. for idx, exist_volume in enumerate(exist_volumes):
  1360. if 'name' in exist_volume and exist_volume['name'] == volume['name']:
  1361. del_idx = idx
  1362. break
  1363. if del_idx != None:
  1364. del exist_volumes[del_idx]
  1365. modified = True
  1366. del_idx = None
  1367. for idx, exist_volume_mount in enumerate(exist_volume_mounts):
  1368. if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']:
  1369. del_idx = idx
  1370. break
  1371. if del_idx != None:
  1372. del exist_volume_mounts[idx]
  1373. modified = True
  1374. return modified
  1375. def add_volume_mount(self, volume_mount):
  1376. ''' add a volume or volume mount to the proper location '''
  1377. exist_volume_mounts = self.get_volume_mounts()
  1378. if not exist_volume_mounts and volume_mount:
  1379. self.put(DeploymentConfig.volume_mounts_path, [volume_mount])
  1380. else:
  1381. exist_volume_mounts.append(volume_mount)
  1382. def add_volume(self, volume):
  1383. ''' add a volume or volume mount to the proper location '''
  1384. exist_volumes = self.get_volumes()
  1385. if not volume:
  1386. return
  1387. if not exist_volumes:
  1388. self.put(DeploymentConfig.volumes_path, [volume])
  1389. else:
  1390. exist_volumes.append(volume)
  1391. def update_replicas(self, replicas):
  1392. ''' update replicas value '''
  1393. self.put(DeploymentConfig.replicas_path, replicas)
  1394. def update_volume(self, volume):
  1395. '''place an env in the env var list'''
  1396. exist_volumes = self.get_volumes()
  1397. if not volume:
  1398. return False
  1399. # update the volume
  1400. update_idx = None
  1401. for idx, exist_vol in enumerate(exist_volumes):
  1402. if exist_vol['name'] == volume['name']:
  1403. update_idx = idx
  1404. break
  1405. if update_idx != None:
  1406. exist_volumes[update_idx] = volume
  1407. else:
  1408. self.add_volume(volume)
  1409. return True
  1410. def update_volume_mount(self, volume_mount):
  1411. '''place an env in the env var list'''
  1412. modified = False
  1413. exist_volume_mounts = self.get_volume_mounts()
  1414. if not volume_mount:
  1415. return False
  1416. # update the volume mount
  1417. for exist_vol_mount in exist_volume_mounts:
  1418. if exist_vol_mount['name'] == volume_mount['name']:
  1419. if 'mountPath' in exist_vol_mount and \
  1420. str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']):
  1421. exist_vol_mount['mountPath'] = volume_mount['mountPath']
  1422. modified = True
  1423. break
  1424. if not modified:
  1425. self.add_volume_mount(volume_mount)
  1426. modified = True
  1427. return modified
  1428. def needs_update_volume(self, volume, volume_mount):
  1429. ''' verify a volume update is needed '''
  1430. exist_volume = self.find_volume_by_name(volume)
  1431. exist_volume_mount = self.find_volume_by_name(volume, mounts=True)
  1432. results = []
  1433. results.append(exist_volume['name'] == volume['name'])
  1434. if 'secret' in volume:
  1435. results.append('secret' in exist_volume)
  1436. results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName'])
  1437. results.append(exist_volume_mount['name'] == volume_mount['name'])
  1438. results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
  1439. elif 'emptyDir' in volume:
  1440. results.append(exist_volume_mount['name'] == volume['name'])
  1441. results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
  1442. elif 'persistentVolumeClaim' in volume:
  1443. pvc = 'persistentVolumeClaim'
  1444. results.append(pvc in exist_volume)
  1445. if results[-1]:
  1446. results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName'])
  1447. if 'claimSize' in volume[pvc]:
  1448. results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize'])
  1449. elif 'hostpath' in volume:
  1450. results.append('hostPath' in exist_volume)
  1451. results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath'])
  1452. return not all(results)
  1453. def needs_update_replicas(self, replicas):
  1454. ''' verify whether a replica update is needed '''
  1455. current_reps = self.get(DeploymentConfig.replicas_path)
  1456. return not current_reps == replicas
  1457. # -*- -*- -*- End included fragment: lib/deploymentconfig.py -*- -*- -*-
  1458. # -*- -*- -*- Begin included fragment: lib/replicationcontroller.py -*- -*- -*-
  1459. # pylint: disable=too-many-public-methods
  1460. class ReplicationController(DeploymentConfig):
  1461. ''' Class to model a replicationcontroller openshift object.
  1462. Currently we are modeled after a deployment config since they
  1463. are very similar. In the future, when the need arises we
  1464. will add functionality to this class.
  1465. '''
  1466. replicas_path = "spec.replicas"
  1467. env_path = "spec.template.spec.containers[0].env"
  1468. volumes_path = "spec.template.spec.volumes"
  1469. container_path = "spec.template.spec.containers"
  1470. volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
  1471. def __init__(self, content):
  1472. ''' Constructor for ReplicationController '''
  1473. super(ReplicationController, self).__init__(content=content)
  1474. # -*- -*- -*- End included fragment: lib/replicationcontroller.py -*- -*- -*-
  1475. # -*- -*- -*- Begin included fragment: class/oc_scale.py -*- -*- -*-
  1476. # pylint: disable=too-many-instance-attributes
  1477. class OCScale(OpenShiftCLI):
  1478. ''' Class to wrap the oc command line tools '''
  1479. # pylint allows 5
  1480. # pylint: disable=too-many-arguments
  1481. def __init__(self,
  1482. resource_name,
  1483. namespace,
  1484. replicas,
  1485. kind,
  1486. kubeconfig='/etc/origin/master/admin.kubeconfig',
  1487. verbose=False):
  1488. ''' Constructor for OCScale '''
  1489. super(OCScale, self).__init__(namespace, kubeconfig=kubeconfig, verbose=verbose)
  1490. self.kind = kind
  1491. self.replicas = replicas
  1492. self.name = resource_name
  1493. self._resource = None
  1494. @property
  1495. def resource(self):
  1496. ''' property function for resource var '''
  1497. if not self._resource:
  1498. self.get()
  1499. return self._resource
  1500. @resource.setter
  1501. def resource(self, data):
  1502. ''' setter function for resource var '''
  1503. self._resource = data
  1504. def get(self):
  1505. '''return replicas information '''
  1506. vol = self._get(self.kind, self.name)
  1507. if vol['returncode'] == 0:
  1508. if self.kind == 'dc':
  1509. # The resource returned from a query could be an rc or dc.
  1510. # pylint: disable=redefined-variable-type
  1511. self.resource = DeploymentConfig(content=vol['results'][0])
  1512. vol['results'] = [self.resource.get_replicas()]
  1513. if self.kind == 'rc':
  1514. # The resource returned from a query could be an rc or dc.
  1515. # pylint: disable=redefined-variable-type
  1516. self.resource = ReplicationController(content=vol['results'][0])
  1517. vol['results'] = [self.resource.get_replicas()]
  1518. return vol
  1519. def put(self):
  1520. '''update replicas into dc '''
  1521. self.resource.update_replicas(self.replicas)
  1522. return self._replace_content(self.kind, self.name, self.resource.yaml_dict)
  1523. def needs_update(self):
  1524. ''' verify whether an update is needed '''
  1525. return self.resource.needs_update_replicas(self.replicas)
  1526. # pylint: disable=too-many-return-statements
  1527. @staticmethod
  1528. def run_ansible(params, check_mode):
  1529. '''perform the idempotent ansible logic'''
  1530. oc_scale = OCScale(params['name'],
  1531. params['namespace'],
  1532. params['replicas'],
  1533. params['kind'],
  1534. params['kubeconfig'],
  1535. verbose=params['debug'])
  1536. state = params['state']
  1537. api_rval = oc_scale.get()
  1538. if api_rval['returncode'] != 0:
  1539. return {'failed': True, 'msg': api_rval}
  1540. #####
  1541. # Get
  1542. #####
  1543. if state == 'list':
  1544. return {'changed': False, 'result': api_rval['results'], 'state': 'list'} # noqa: E501
  1545. elif state == 'present':
  1546. ########
  1547. # Update
  1548. ########
  1549. if oc_scale.needs_update():
  1550. if check_mode:
  1551. return {'changed': True, 'result': 'CHECK_MODE: Would have updated.'} # noqa: E501
  1552. api_rval = oc_scale.put()
  1553. if api_rval['returncode'] != 0:
  1554. return {'failed': True, 'msg': api_rval}
  1555. # return the created object
  1556. api_rval = oc_scale.get()
  1557. if api_rval['returncode'] != 0:
  1558. return {'failed': True, 'msg': api_rval}
  1559. return {'changed': True, 'result': api_rval['results'], 'state': 'present'} # noqa: E501
  1560. return {'changed': False, 'result': api_rval['results'], 'state': 'present'} # noqa: E501
  1561. return {'failed': True, 'msg': 'Unknown state passed. [{}]'.format(state)}
  1562. # -*- -*- -*- End included fragment: class/oc_scale.py -*- -*- -*-
  1563. # -*- -*- -*- Begin included fragment: ansible/oc_scale.py -*- -*- -*-
  1564. def main():
  1565. '''
  1566. ansible oc module for scaling
  1567. '''
  1568. module = AnsibleModule(
  1569. argument_spec=dict(
  1570. kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
  1571. state=dict(default='present', type='str', choices=['present', 'list']),
  1572. debug=dict(default=False, type='bool'),
  1573. kind=dict(default='dc', choices=['dc', 'rc'], type='str'),
  1574. namespace=dict(default='default', type='str'),
  1575. replicas=dict(default=None, type='int'),
  1576. name=dict(default=None, type='str'),
  1577. ),
  1578. supports_check_mode=True,
  1579. )
  1580. rval = OCScale.run_ansible(module.params, module.check_mode)
  1581. if 'failed' in rval:
  1582. module.fail_json(**rval)
  1583. module.exit_json(**rval)
  1584. if __name__ == '__main__':
  1585. main()
  1586. # -*- -*- -*- End included fragment: ansible/oc_scale.py -*- -*- -*-