oc_adm_router.py 109 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254
  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/router -*- -*- -*-
  51. DOCUMENTATION = '''
  52. ---
  53. module: oc_adm_router
  54. short_description: Module to manage openshift router
  55. description:
  56. - Manage openshift router programmatically.
  57. options:
  58. state:
  59. description:
  60. - Whether to create or delete the router
  61. - present - create the router
  62. - absent - remove the router
  63. - list - return the current representation of a router
  64. required: false
  65. default: present
  66. choices:
  67. - present
  68. - absent
  69. aliases: []
  70. kubeconfig:
  71. description:
  72. - The path for the kubeconfig file to use for authentication
  73. required: false
  74. default: /etc/origin/master/admin.kubeconfig
  75. aliases: []
  76. debug:
  77. description:
  78. - Turn on debug output.
  79. required: false
  80. default: False
  81. aliases: []
  82. name:
  83. description:
  84. - The name of the router
  85. required: false
  86. default: router
  87. aliases: []
  88. namespace:
  89. description:
  90. - The namespace where to manage the router.
  91. required: false
  92. default: default
  93. aliases: []
  94. images:
  95. description:
  96. - The image to base this router on - ${component} will be replaced with --type
  97. required: 'registry.access.redhat.com/openshift3/ose-${component}:${version}'
  98. default: None
  99. aliases: []
  100. latest_images:
  101. description:
  102. - If true, attempt to use the latest image for the registry instead of the latest release.
  103. required: false
  104. default: False
  105. aliases: []
  106. labels:
  107. description:
  108. - A set of labels to uniquely identify the registry and its components.
  109. required: false
  110. default: None
  111. aliases: []
  112. ports:
  113. description:
  114. - A list of strings in the 'port:port' format
  115. required: False
  116. default:
  117. - 80:80
  118. - 443:443
  119. aliases: []
  120. replicas:
  121. description:
  122. - The replication factor of the registry; commonly 2 when high availability is desired.
  123. required: False
  124. default: 1
  125. aliases: []
  126. selector:
  127. description:
  128. - Selector used to filter nodes on deployment. Used to run routers on a specific set of nodes.
  129. required: False
  130. default: None
  131. aliases: []
  132. service_account:
  133. description:
  134. - Name of the service account to use to run the router pod.
  135. required: False
  136. default: router
  137. aliases: []
  138. router_type:
  139. description:
  140. - The router image to use - if you specify --images this flag may be ignored.
  141. required: false
  142. default: haproxy-router
  143. aliases: []
  144. extended_validation:
  145. description:
  146. - If true, configure the router to perform extended validation on routes before admitting them.
  147. required: false
  148. default: True
  149. aliases: []
  150. external_host:
  151. description:
  152. - If the underlying router implementation connects with an external host, this is the external host's hostname.
  153. required: false
  154. default: None
  155. aliases: []
  156. external_host_vserver:
  157. description:
  158. - If the underlying router implementation uses virtual servers, this is the name of the virtual server for HTTP connections.
  159. required: false
  160. default: None
  161. aliases: []
  162. external_host_insecure:
  163. description:
  164. - If the underlying router implementation connects with an external host
  165. - over a secure connection, this causes the router to skip strict certificate verification with the external host.
  166. required: false
  167. default: False
  168. aliases: []
  169. external_host_partition_path:
  170. description:
  171. - If the underlying router implementation uses partitions for control boundaries, this is the path to use for that partition.
  172. required: false
  173. default: None
  174. aliases: []
  175. external_host_username:
  176. description:
  177. - If the underlying router implementation connects with an external host, this is the username for authenticating with the external host.
  178. required: false
  179. default: None
  180. aliases: []
  181. external_host_password:
  182. description:
  183. - If the underlying router implementation connects with an external host, this is the password for authenticating with the external host.
  184. required: false
  185. default: None
  186. aliases: []
  187. external_host_private_key:
  188. description:
  189. - If the underlying router implementation requires an SSH private key, this is the path to the private key file.
  190. required: false
  191. default: None
  192. aliases: []
  193. author:
  194. - "Kenny Woodson <kwoodson@redhat.com>"
  195. extends_documentation_fragment:
  196. - There are some exceptions to note when doing the idempotency in this module.
  197. - The strategy is to use the oc adm router command to generate a default
  198. - configuration when creating or updating a router. Often times there
  199. - differences from the generated template and what is in memory in openshift.
  200. - We make exceptions to not check these specific values when comparing objects.
  201. - Here are a list of exceptions:
  202. - - DeploymentConfig:
  203. - dnsPolicy
  204. - terminationGracePeriodSeconds
  205. - restartPolicy
  206. - timeoutSeconds
  207. - livenessProbe
  208. - readinessProbe
  209. - terminationMessagePath
  210. - hostPort
  211. - defaultMode
  212. - Service:
  213. - portalIP
  214. - clusterIP
  215. - sessionAffinity
  216. - type
  217. - ServiceAccount:
  218. - secrets
  219. - imagePullSecrets
  220. '''
  221. EXAMPLES = '''
  222. - name: create routers
  223. oc_adm_router:
  224. name: router
  225. service_account: router
  226. replicas: 2
  227. namespace: default
  228. selector: type=infra
  229. cert_file: /etc/origin/master/named_certificates/router.crt
  230. key_file: /etc/origin/master/named_certificates/router.key
  231. cacert_file: /etc/origin/master/named_certificates/router.ca
  232. edits:
  233. - key: spec.strategy.rollingParams
  234. value:
  235. intervalSeconds: 1
  236. maxSurge: 50%
  237. maxUnavailable: 50%
  238. timeoutSeconds: 600
  239. updatePeriodSeconds: 1
  240. action: put
  241. - key: spec.template.spec.containers[0].resources.limits.memory
  242. value: 2G
  243. action: put
  244. - key: spec.template.spec.containers[0].resources.requests.memory
  245. value: 1G
  246. action: put
  247. - key: spec.template.spec.containers[0].env
  248. value:
  249. name: ROUTER_MAX_CONNECTIONS
  250. value: "10000"
  251. action: update
  252. register: router_out
  253. run_once: True
  254. '''
  255. # -*- -*- -*- End included fragment: doc/router -*- -*- -*-
  256. # -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
  257. class YeditException(Exception): # pragma: no cover
  258. ''' Exception class for Yedit '''
  259. pass
  260. # pylint: disable=too-many-public-methods,too-many-instance-attributes
  261. class Yedit(object): # pragma: no cover
  262. ''' Class to modify yaml files '''
  263. re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
  264. re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
  265. com_sep = set(['.', '#', '|', ':'])
  266. # pylint: disable=too-many-arguments
  267. def __init__(self,
  268. filename=None,
  269. content=None,
  270. content_type='yaml',
  271. separator='.',
  272. backup_ext=None,
  273. backup=False):
  274. self.content = content
  275. self._separator = separator
  276. self.filename = filename
  277. self.__yaml_dict = content
  278. self.content_type = content_type
  279. self.backup = backup
  280. if backup_ext is None:
  281. self.backup_ext = ".{}".format(time.strftime("%Y%m%dT%H%M%S"))
  282. else:
  283. self.backup_ext = backup_ext
  284. self.load(content_type=self.content_type)
  285. if self.__yaml_dict is None:
  286. self.__yaml_dict = {}
  287. @property
  288. def separator(self):
  289. ''' getter method for separator '''
  290. return self._separator
  291. @separator.setter
  292. def separator(self, inc_sep):
  293. ''' setter method for separator '''
  294. self._separator = inc_sep
  295. @property
  296. def yaml_dict(self):
  297. ''' getter method for yaml_dict '''
  298. return self.__yaml_dict
  299. @yaml_dict.setter
  300. def yaml_dict(self, value):
  301. ''' setter method for yaml_dict '''
  302. self.__yaml_dict = value
  303. @staticmethod
  304. def parse_key(key, sep='.'):
  305. '''parse the key allowing the appropriate separator'''
  306. common_separators = list(Yedit.com_sep - set([sep]))
  307. return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
  308. @staticmethod
  309. def valid_key(key, sep='.'):
  310. '''validate the incoming key'''
  311. common_separators = list(Yedit.com_sep - set([sep]))
  312. if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
  313. return False
  314. return True
  315. # pylint: disable=too-many-return-statements,too-many-branches
  316. @staticmethod
  317. def remove_entry(data, key, index=None, value=None, sep='.'):
  318. ''' remove data at location key '''
  319. if key == '' and isinstance(data, dict):
  320. if value is not None:
  321. data.pop(value)
  322. elif index is not None:
  323. raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
  324. else:
  325. data.clear()
  326. return True
  327. elif key == '' and isinstance(data, list):
  328. ind = None
  329. if value is not None:
  330. try:
  331. ind = data.index(value)
  332. except ValueError:
  333. return False
  334. elif index is not None:
  335. ind = index
  336. else:
  337. del data[:]
  338. if ind is not None:
  339. data.pop(ind)
  340. return True
  341. if not (key and Yedit.valid_key(key, sep)) and \
  342. isinstance(data, (list, dict)):
  343. return None
  344. key_indexes = Yedit.parse_key(key, sep)
  345. for arr_ind, dict_key in key_indexes[:-1]:
  346. if dict_key and isinstance(data, dict):
  347. data = data.get(dict_key)
  348. elif (arr_ind and isinstance(data, list) and
  349. int(arr_ind) <= len(data) - 1):
  350. data = data[int(arr_ind)]
  351. else:
  352. return None
  353. # process last index for remove
  354. # expected list entry
  355. if key_indexes[-1][0]:
  356. if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  357. del data[int(key_indexes[-1][0])]
  358. return True
  359. # expected dict entry
  360. elif key_indexes[-1][1]:
  361. if isinstance(data, dict):
  362. del data[key_indexes[-1][1]]
  363. return True
  364. @staticmethod
  365. def add_entry(data, key, item=None, sep='.'):
  366. ''' Get an item from a dictionary with key notation a.b.c
  367. d = {'a': {'b': 'c'}}}
  368. key = a#b
  369. return c
  370. '''
  371. if key == '':
  372. pass
  373. elif (not (key and Yedit.valid_key(key, sep)) and
  374. isinstance(data, (list, dict))):
  375. return None
  376. key_indexes = Yedit.parse_key(key, sep)
  377. for arr_ind, dict_key in key_indexes[:-1]:
  378. if dict_key:
  379. if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
  380. data = data[dict_key]
  381. continue
  382. elif data and not isinstance(data, dict):
  383. raise YeditException("Unexpected item type found while going through key " +
  384. "path: {} (at key: {})".format(key, dict_key))
  385. data[dict_key] = {}
  386. data = data[dict_key]
  387. elif (arr_ind and isinstance(data, list) and
  388. int(arr_ind) <= len(data) - 1):
  389. data = data[int(arr_ind)]
  390. else:
  391. raise YeditException("Unexpected item type found while going through key path: {}".format(key))
  392. if key == '':
  393. data = item
  394. # process last index for add
  395. # expected list entry
  396. elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  397. data[int(key_indexes[-1][0])] = item
  398. # expected dict entry
  399. elif key_indexes[-1][1] and isinstance(data, dict):
  400. data[key_indexes[-1][1]] = item
  401. # didn't add/update to an existing list, nor add/update key to a dict
  402. # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
  403. # non-existent array
  404. else:
  405. raise YeditException("Error adding to object at path: {}".format(key))
  406. return data
  407. @staticmethod
  408. def get_entry(data, key, sep='.'):
  409. ''' Get an item from a dictionary with key notation a.b.c
  410. d = {'a': {'b': 'c'}}}
  411. key = a.b
  412. return c
  413. '''
  414. if key == '':
  415. pass
  416. elif (not (key and Yedit.valid_key(key, sep)) and
  417. isinstance(data, (list, dict))):
  418. return None
  419. key_indexes = Yedit.parse_key(key, sep)
  420. for arr_ind, dict_key in key_indexes:
  421. if dict_key and isinstance(data, dict):
  422. data = data.get(dict_key)
  423. elif (arr_ind and isinstance(data, list) and
  424. int(arr_ind) <= len(data) - 1):
  425. data = data[int(arr_ind)]
  426. else:
  427. return None
  428. return data
  429. @staticmethod
  430. def _write(filename, contents):
  431. ''' Actually write the file contents to disk. This helps with mocking. '''
  432. tmp_filename = filename + '.yedit'
  433. with open(tmp_filename, 'w') as yfd:
  434. fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  435. yfd.write(contents)
  436. fcntl.flock(yfd, fcntl.LOCK_UN)
  437. os.rename(tmp_filename, filename)
  438. def write(self):
  439. ''' write to file '''
  440. if not self.filename:
  441. raise YeditException('Please specify a filename.')
  442. if self.backup and self.file_exists():
  443. shutil.copy(self.filename, '{}{}'.format(self.filename, self.backup_ext))
  444. # Try to set format attributes if supported
  445. try:
  446. self.yaml_dict.fa.set_block_style()
  447. except AttributeError:
  448. pass
  449. # Try to use RoundTripDumper if supported.
  450. if self.content_type == 'yaml':
  451. try:
  452. Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
  453. except AttributeError:
  454. Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
  455. elif self.content_type == 'json':
  456. Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
  457. else:
  458. raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
  459. 'Please specify a content_type of yaml or json.')
  460. return (True, self.yaml_dict)
  461. def read(self):
  462. ''' read from file '''
  463. # check if it exists
  464. if self.filename is None or not self.file_exists():
  465. return None
  466. contents = None
  467. with open(self.filename) as yfd:
  468. contents = yfd.read()
  469. return contents
  470. def file_exists(self):
  471. ''' return whether file exists '''
  472. if os.path.exists(self.filename):
  473. return True
  474. return False
  475. def load(self, content_type='yaml'):
  476. ''' return yaml file '''
  477. contents = self.read()
  478. if not contents and not self.content:
  479. return None
  480. if self.content:
  481. if isinstance(self.content, dict):
  482. self.yaml_dict = self.content
  483. return self.yaml_dict
  484. elif isinstance(self.content, str):
  485. contents = self.content
  486. # check if it is yaml
  487. try:
  488. if content_type == 'yaml' and contents:
  489. # Try to set format attributes if supported
  490. try:
  491. self.yaml_dict.fa.set_block_style()
  492. except AttributeError:
  493. pass
  494. # Try to use RoundTripLoader if supported.
  495. try:
  496. self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
  497. except AttributeError:
  498. self.yaml_dict = yaml.safe_load(contents)
  499. # Try to set format attributes if supported
  500. try:
  501. self.yaml_dict.fa.set_block_style()
  502. except AttributeError:
  503. pass
  504. elif content_type == 'json' and contents:
  505. self.yaml_dict = json.loads(contents)
  506. except yaml.YAMLError as err:
  507. # Error loading yaml or json
  508. raise YeditException('Problem with loading yaml file. {}'.format(err))
  509. return self.yaml_dict
  510. def get(self, key):
  511. ''' get a specified key'''
  512. try:
  513. entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
  514. except KeyError:
  515. entry = None
  516. return entry
  517. def pop(self, path, key_or_item):
  518. ''' remove a key, value pair from a dict or an item for a list'''
  519. try:
  520. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  521. except KeyError:
  522. entry = None
  523. if entry is None:
  524. return (False, self.yaml_dict)
  525. if isinstance(entry, dict):
  526. # AUDIT:maybe-no-member makes sense due to fuzzy types
  527. # pylint: disable=maybe-no-member
  528. if key_or_item in entry:
  529. entry.pop(key_or_item)
  530. return (True, self.yaml_dict)
  531. return (False, self.yaml_dict)
  532. elif isinstance(entry, list):
  533. # AUDIT:maybe-no-member makes sense due to fuzzy types
  534. # pylint: disable=maybe-no-member
  535. ind = None
  536. try:
  537. ind = entry.index(key_or_item)
  538. except ValueError:
  539. return (False, self.yaml_dict)
  540. entry.pop(ind)
  541. return (True, self.yaml_dict)
  542. return (False, self.yaml_dict)
  543. def delete(self, path, index=None, value=None):
  544. ''' remove path from a dict'''
  545. try:
  546. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  547. except KeyError:
  548. entry = None
  549. if entry is None:
  550. return (False, self.yaml_dict)
  551. result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
  552. if not result:
  553. return (False, self.yaml_dict)
  554. return (True, self.yaml_dict)
  555. def exists(self, path, value):
  556. ''' check if value exists at path'''
  557. try:
  558. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  559. except KeyError:
  560. entry = None
  561. if isinstance(entry, list):
  562. if value in entry:
  563. return True
  564. return False
  565. elif isinstance(entry, dict):
  566. if isinstance(value, dict):
  567. rval = False
  568. for key, val in value.items():
  569. if entry[key] != val:
  570. rval = False
  571. break
  572. else:
  573. rval = True
  574. return rval
  575. return value in entry
  576. return entry == value
  577. def append(self, path, value):
  578. '''append value to a list'''
  579. try:
  580. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  581. except KeyError:
  582. entry = None
  583. if entry is None:
  584. self.put(path, [])
  585. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  586. if not isinstance(entry, list):
  587. return (False, self.yaml_dict)
  588. # AUDIT:maybe-no-member makes sense due to loading data from
  589. # a serialized format.
  590. # pylint: disable=maybe-no-member
  591. entry.append(value)
  592. return (True, self.yaml_dict)
  593. # pylint: disable=too-many-arguments
  594. def update(self, path, value, index=None, curr_value=None):
  595. ''' put path, value into a dict '''
  596. try:
  597. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  598. except KeyError:
  599. entry = None
  600. if isinstance(entry, dict):
  601. # AUDIT:maybe-no-member makes sense due to fuzzy types
  602. # pylint: disable=maybe-no-member
  603. if not isinstance(value, dict):
  604. raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
  605. 'value=[{}] type=[{}]'.format(value, type(value)))
  606. entry.update(value)
  607. return (True, self.yaml_dict)
  608. elif isinstance(entry, list):
  609. # AUDIT:maybe-no-member makes sense due to fuzzy types
  610. # pylint: disable=maybe-no-member
  611. ind = None
  612. if curr_value:
  613. try:
  614. ind = entry.index(curr_value)
  615. except ValueError:
  616. return (False, self.yaml_dict)
  617. elif index is not None:
  618. ind = index
  619. if ind is not None and entry[ind] != value:
  620. entry[ind] = value
  621. return (True, self.yaml_dict)
  622. # see if it exists in the list
  623. try:
  624. ind = entry.index(value)
  625. except ValueError:
  626. # doesn't exist, append it
  627. entry.append(value)
  628. return (True, self.yaml_dict)
  629. # already exists, return
  630. if ind is not None:
  631. return (False, self.yaml_dict)
  632. return (False, self.yaml_dict)
  633. def put(self, path, value):
  634. ''' put path, value into a dict '''
  635. try:
  636. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  637. except KeyError:
  638. entry = None
  639. if entry == value:
  640. return (False, self.yaml_dict)
  641. # deepcopy didn't work
  642. # Try to use ruamel.yaml and fallback to pyyaml
  643. try:
  644. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  645. default_flow_style=False),
  646. yaml.RoundTripLoader)
  647. except AttributeError:
  648. tmp_copy = copy.deepcopy(self.yaml_dict)
  649. # set the format attributes if available
  650. try:
  651. tmp_copy.fa.set_block_style()
  652. except AttributeError:
  653. pass
  654. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  655. if result is None:
  656. return (False, self.yaml_dict)
  657. # When path equals "" it is a special case.
  658. # "" refers to the root of the document
  659. # Only update the root path (entire document) when its a list or dict
  660. if path == '':
  661. if isinstance(result, list) or isinstance(result, dict):
  662. self.yaml_dict = result
  663. return (True, self.yaml_dict)
  664. return (False, self.yaml_dict)
  665. self.yaml_dict = tmp_copy
  666. return (True, self.yaml_dict)
  667. def create(self, path, value):
  668. ''' create a yaml file '''
  669. if not self.file_exists():
  670. # deepcopy didn't work
  671. # Try to use ruamel.yaml and fallback to pyyaml
  672. try:
  673. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  674. default_flow_style=False),
  675. yaml.RoundTripLoader)
  676. except AttributeError:
  677. tmp_copy = copy.deepcopy(self.yaml_dict)
  678. # set the format attributes if available
  679. try:
  680. tmp_copy.fa.set_block_style()
  681. except AttributeError:
  682. pass
  683. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  684. if result is not None:
  685. self.yaml_dict = tmp_copy
  686. return (True, self.yaml_dict)
  687. return (False, self.yaml_dict)
  688. @staticmethod
  689. def get_curr_value(invalue, val_type):
  690. '''return the current value'''
  691. if invalue is None:
  692. return None
  693. curr_value = invalue
  694. if val_type == 'yaml':
  695. curr_value = yaml.safe_load(str(invalue))
  696. elif val_type == 'json':
  697. curr_value = json.loads(invalue)
  698. return curr_value
  699. @staticmethod
  700. def parse_value(inc_value, vtype=''):
  701. '''determine value type passed'''
  702. true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
  703. 'on', 'On', 'ON', ]
  704. false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
  705. 'off', 'Off', 'OFF']
  706. # It came in as a string but you didn't specify value_type as string
  707. # we will convert to bool if it matches any of the above cases
  708. if isinstance(inc_value, str) and 'bool' in vtype:
  709. if inc_value not in true_bools and inc_value not in false_bools:
  710. raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
  711. elif isinstance(inc_value, bool) and 'str' in vtype:
  712. inc_value = str(inc_value)
  713. # There is a special case where '' will turn into None after yaml loading it so skip
  714. if isinstance(inc_value, str) and inc_value == '':
  715. pass
  716. # If vtype is not str then go ahead and attempt to yaml load it.
  717. elif isinstance(inc_value, str) and 'str' not in vtype:
  718. try:
  719. inc_value = yaml.safe_load(inc_value)
  720. except Exception:
  721. raise YeditException('Could not determine type of incoming value. ' +
  722. 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
  723. return inc_value
  724. @staticmethod
  725. def process_edits(edits, yamlfile):
  726. '''run through a list of edits and process them one-by-one'''
  727. results = []
  728. for edit in edits:
  729. value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
  730. if edit.get('action') == 'update':
  731. # pylint: disable=line-too-long
  732. curr_value = Yedit.get_curr_value(
  733. Yedit.parse_value(edit.get('curr_value')),
  734. edit.get('curr_value_format'))
  735. rval = yamlfile.update(edit['key'],
  736. value,
  737. edit.get('index'),
  738. curr_value)
  739. elif edit.get('action') == 'append':
  740. rval = yamlfile.append(edit['key'], value)
  741. else:
  742. rval = yamlfile.put(edit['key'], value)
  743. if rval[0]:
  744. results.append({'key': edit['key'], 'edit': rval[1]})
  745. return {'changed': len(results) > 0, 'results': results}
  746. # pylint: disable=too-many-return-statements,too-many-branches
  747. @staticmethod
  748. def run_ansible(params):
  749. '''perform the idempotent crud operations'''
  750. yamlfile = Yedit(filename=params['src'],
  751. backup=params['backup'],
  752. content_type=params['content_type'],
  753. backup_ext=params['backup_ext'],
  754. separator=params['separator'])
  755. state = params['state']
  756. if params['src']:
  757. rval = yamlfile.load()
  758. if yamlfile.yaml_dict is None and state != 'present':
  759. return {'failed': True,
  760. 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
  761. 'file exists, that it is has correct permissions, and is valid yaml.'}
  762. if state == 'list':
  763. if params['content']:
  764. content = Yedit.parse_value(params['content'], params['content_type'])
  765. yamlfile.yaml_dict = content
  766. if params['key']:
  767. rval = yamlfile.get(params['key'])
  768. return {'changed': False, 'result': rval, 'state': state}
  769. elif state == 'absent':
  770. if params['content']:
  771. content = Yedit.parse_value(params['content'], params['content_type'])
  772. yamlfile.yaml_dict = content
  773. if params['update']:
  774. rval = yamlfile.pop(params['key'], params['value'])
  775. else:
  776. rval = yamlfile.delete(params['key'], params['index'], params['value'])
  777. if rval[0] and params['src']:
  778. yamlfile.write()
  779. return {'changed': rval[0], 'result': rval[1], 'state': state}
  780. elif state == 'present':
  781. # check if content is different than what is in the file
  782. if params['content']:
  783. content = Yedit.parse_value(params['content'], params['content_type'])
  784. # We had no edits to make and the contents are the same
  785. if yamlfile.yaml_dict == content and \
  786. params['value'] is None:
  787. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  788. yamlfile.yaml_dict = content
  789. # If we were passed a key, value then
  790. # we enapsulate it in a list and process it
  791. # Key, Value passed to the module : Converted to Edits list #
  792. edits = []
  793. _edit = {}
  794. if params['value'] is not None:
  795. _edit['value'] = params['value']
  796. _edit['value_type'] = params['value_type']
  797. _edit['key'] = params['key']
  798. if params['update']:
  799. _edit['action'] = 'update'
  800. _edit['curr_value'] = params['curr_value']
  801. _edit['curr_value_format'] = params['curr_value_format']
  802. _edit['index'] = params['index']
  803. elif params['append']:
  804. _edit['action'] = 'append'
  805. edits.append(_edit)
  806. elif params['edits'] is not None:
  807. edits = params['edits']
  808. if edits:
  809. results = Yedit.process_edits(edits, yamlfile)
  810. # if there were changes and a src provided to us we need to write
  811. if results['changed'] and params['src']:
  812. yamlfile.write()
  813. return {'changed': results['changed'], 'result': results['results'], 'state': state}
  814. # no edits to make
  815. if params['src']:
  816. # pylint: disable=redefined-variable-type
  817. rval = yamlfile.write()
  818. return {'changed': rval[0],
  819. 'result': rval[1],
  820. 'state': state}
  821. # We were passed content but no src, key or value, or edits. Return contents in memory
  822. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  823. return {'failed': True, 'msg': 'Unkown state passed'}
  824. # -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
  825. # -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
  826. # pylint: disable=too-many-lines
  827. # noqa: E301,E302,E303,T001
  828. class OpenShiftCLIError(Exception):
  829. '''Exception class for openshiftcli'''
  830. pass
  831. ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
  832. def locate_oc_binary():
  833. ''' Find and return oc binary file '''
  834. # https://github.com/openshift/openshift-ansible/issues/3410
  835. # oc can be in /usr/local/bin in some cases, but that may not
  836. # be in $PATH due to ansible/sudo
  837. paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
  838. oc_binary = 'oc'
  839. # Use shutil.which if it is available, otherwise fallback to a naive path search
  840. try:
  841. which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
  842. if which_result is not None:
  843. oc_binary = which_result
  844. except AttributeError:
  845. for path in paths:
  846. if os.path.exists(os.path.join(path, oc_binary)):
  847. oc_binary = os.path.join(path, oc_binary)
  848. break
  849. return oc_binary
  850. # pylint: disable=too-few-public-methods
  851. class OpenShiftCLI(object):
  852. ''' Class to wrap the command line tools '''
  853. def __init__(self,
  854. namespace,
  855. kubeconfig='/etc/origin/master/admin.kubeconfig',
  856. verbose=False,
  857. all_namespaces=False):
  858. ''' Constructor for OpenshiftCLI '''
  859. self.namespace = namespace
  860. self.verbose = verbose
  861. self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
  862. self.all_namespaces = all_namespaces
  863. self.oc_binary = locate_oc_binary()
  864. # Pylint allows only 5 arguments to be passed.
  865. # pylint: disable=too-many-arguments
  866. def _replace_content(self, resource, rname, content, edits=None, force=False, sep='.'):
  867. ''' replace the current object with the content '''
  868. res = self._get(resource, rname)
  869. if not res['results']:
  870. return res
  871. fname = Utils.create_tmpfile(rname + '-')
  872. yed = Yedit(fname, res['results'][0], separator=sep)
  873. updated = False
  874. if content is not None:
  875. changes = []
  876. for key, value in content.items():
  877. changes.append(yed.put(key, value))
  878. if any([change[0] for change in changes]):
  879. updated = True
  880. elif edits is not None:
  881. results = Yedit.process_edits(edits, yed)
  882. if results['changed']:
  883. updated = True
  884. if updated:
  885. yed.write()
  886. atexit.register(Utils.cleanup, [fname])
  887. return self._replace(fname, force)
  888. return {'returncode': 0, 'updated': False}
  889. def _replace(self, fname, force=False):
  890. '''replace the current object with oc replace'''
  891. # We are removing the 'resourceVersion' to handle
  892. # a race condition when modifying oc objects
  893. yed = Yedit(fname)
  894. results = yed.delete('metadata.resourceVersion')
  895. if results[0]:
  896. yed.write()
  897. cmd = ['replace', '-f', fname]
  898. if force:
  899. cmd.append('--force')
  900. return self.openshift_cmd(cmd)
  901. def _create_from_content(self, rname, content):
  902. '''create a temporary file and then call oc create on it'''
  903. fname = Utils.create_tmpfile(rname + '-')
  904. yed = Yedit(fname, content=content)
  905. yed.write()
  906. atexit.register(Utils.cleanup, [fname])
  907. return self._create(fname)
  908. def _create(self, fname):
  909. '''call oc create on a filename'''
  910. return self.openshift_cmd(['create', '-f', fname])
  911. def _delete(self, resource, name=None, selector=None):
  912. '''call oc delete on a resource'''
  913. cmd = ['delete', resource]
  914. if selector is not None:
  915. cmd.append('--selector={}'.format(selector))
  916. elif name is not None:
  917. cmd.append(name)
  918. else:
  919. raise OpenShiftCLIError('Either name or selector is required when calling delete.')
  920. return self.openshift_cmd(cmd)
  921. def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
  922. '''process a template
  923. template_name: the name of the template to process
  924. create: whether to send to oc create after processing
  925. params: the parameters for the template
  926. template_data: the incoming template's data; instead of a file
  927. '''
  928. cmd = ['process']
  929. if template_data:
  930. cmd.extend(['-f', '-'])
  931. else:
  932. cmd.append(template_name)
  933. if params:
  934. param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
  935. cmd.append('-p')
  936. cmd.extend(param_str)
  937. results = self.openshift_cmd(cmd, output=True, input_data=template_data)
  938. if results['returncode'] != 0 or not create:
  939. return results
  940. fname = Utils.create_tmpfile(template_name + '-')
  941. yed = Yedit(fname, results['results'])
  942. yed.write()
  943. atexit.register(Utils.cleanup, [fname])
  944. return self.openshift_cmd(['create', '-f', fname])
  945. def _get(self, resource, name=None, selector=None, field_selector=None):
  946. '''return a resource by name '''
  947. cmd = ['get', resource]
  948. if selector is not None:
  949. cmd.append('--selector={}'.format(selector))
  950. if field_selector is not None:
  951. cmd.append('--field-selector={}'.format(field_selector))
  952. # Name cannot be used with selector or field_selector.
  953. if selector is None and field_selector is None and name is not None:
  954. cmd.append(name)
  955. cmd.extend(['-o', 'json'])
  956. rval = self.openshift_cmd(cmd, output=True)
  957. # Ensure results are retuned in an array
  958. if 'items' in rval:
  959. rval['results'] = rval['items']
  960. elif not isinstance(rval['results'], list):
  961. rval['results'] = [rval['results']]
  962. return rval
  963. def _schedulable(self, node=None, selector=None, schedulable=True):
  964. ''' perform oadm manage-node scheduable '''
  965. cmd = ['manage-node']
  966. if node:
  967. cmd.extend(node)
  968. else:
  969. cmd.append('--selector={}'.format(selector))
  970. cmd.append('--schedulable={}'.format(schedulable))
  971. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
  972. def _list_pods(self, node=None, selector=None, pod_selector=None):
  973. ''' perform oadm list pods
  974. node: the node in which to list pods
  975. selector: the label selector filter if provided
  976. pod_selector: the pod selector filter if provided
  977. '''
  978. cmd = ['manage-node']
  979. if node:
  980. cmd.extend(node)
  981. else:
  982. cmd.append('--selector={}'.format(selector))
  983. if pod_selector:
  984. cmd.append('--pod-selector={}'.format(pod_selector))
  985. cmd.extend(['--list-pods', '-o', 'json'])
  986. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  987. # pylint: disable=too-many-arguments
  988. def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
  989. ''' perform oadm manage-node evacuate '''
  990. cmd = ['manage-node']
  991. if node:
  992. cmd.extend(node)
  993. else:
  994. cmd.append('--selector={}'.format(selector))
  995. if dry_run:
  996. cmd.append('--dry-run')
  997. if pod_selector:
  998. cmd.append('--pod-selector={}'.format(pod_selector))
  999. if grace_period:
  1000. cmd.append('--grace-period={}'.format(int(grace_period)))
  1001. if force:
  1002. cmd.append('--force')
  1003. cmd.append('--evacuate')
  1004. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  1005. def _version(self):
  1006. ''' return the openshift version'''
  1007. return self.openshift_cmd(['version'], output=True, output_type='raw')
  1008. def _import_image(self, url=None, name=None, tag=None):
  1009. ''' perform image import '''
  1010. cmd = ['import-image']
  1011. image = '{0}'.format(name)
  1012. if tag:
  1013. image += ':{0}'.format(tag)
  1014. cmd.append(image)
  1015. if url:
  1016. cmd.append('--from={0}/{1}'.format(url, image))
  1017. cmd.append('-n{0}'.format(self.namespace))
  1018. cmd.append('--confirm')
  1019. return self.openshift_cmd(cmd)
  1020. def _run(self, cmds, input_data):
  1021. ''' Actually executes the command. This makes mocking easier. '''
  1022. curr_env = os.environ.copy()
  1023. curr_env.update({'KUBECONFIG': self.kubeconfig})
  1024. proc = subprocess.Popen(cmds,
  1025. stdin=subprocess.PIPE,
  1026. stdout=subprocess.PIPE,
  1027. stderr=subprocess.PIPE,
  1028. env=curr_env)
  1029. stdout, stderr = proc.communicate(input_data)
  1030. return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
  1031. # pylint: disable=too-many-arguments,too-many-branches
  1032. def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
  1033. '''Base command for oc '''
  1034. cmds = [self.oc_binary]
  1035. if oadm:
  1036. cmds.append('adm')
  1037. cmds.extend(cmd)
  1038. if self.all_namespaces:
  1039. cmds.extend(['--all-namespaces'])
  1040. elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
  1041. cmds.extend(['-n', self.namespace])
  1042. if self.verbose:
  1043. print(' '.join(cmds))
  1044. try:
  1045. returncode, stdout, stderr = self._run(cmds, input_data)
  1046. except OSError as ex:
  1047. returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
  1048. rval = {"returncode": returncode,
  1049. "cmd": ' '.join(cmds)}
  1050. if output_type == 'json':
  1051. rval['results'] = {}
  1052. if output and stdout:
  1053. try:
  1054. rval['results'] = json.loads(stdout)
  1055. except ValueError as verr:
  1056. if "No JSON object could be decoded" in verr.args:
  1057. rval['err'] = verr.args
  1058. elif output_type == 'raw':
  1059. rval['results'] = stdout if output else ''
  1060. if self.verbose:
  1061. print("STDOUT: {0}".format(stdout))
  1062. print("STDERR: {0}".format(stderr))
  1063. if 'err' in rval or returncode != 0:
  1064. rval.update({"stderr": stderr,
  1065. "stdout": stdout})
  1066. return rval
  1067. class Utils(object): # pragma: no cover
  1068. ''' utilities for openshiftcli modules '''
  1069. @staticmethod
  1070. def _write(filename, contents):
  1071. ''' Actually write the file contents to disk. This helps with mocking. '''
  1072. with open(filename, 'w') as sfd:
  1073. sfd.write(str(contents))
  1074. @staticmethod
  1075. def create_tmp_file_from_contents(rname, data, ftype='yaml'):
  1076. ''' create a file in tmp with name and contents'''
  1077. tmp = Utils.create_tmpfile(prefix=rname)
  1078. if ftype == 'yaml':
  1079. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  1080. # pylint: disable=no-member
  1081. if hasattr(yaml, 'RoundTripDumper'):
  1082. Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
  1083. else:
  1084. Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
  1085. elif ftype == 'json':
  1086. Utils._write(tmp, json.dumps(data))
  1087. else:
  1088. Utils._write(tmp, data)
  1089. # Register cleanup when module is done
  1090. atexit.register(Utils.cleanup, [tmp])
  1091. return tmp
  1092. @staticmethod
  1093. def create_tmpfile_copy(inc_file):
  1094. '''create a temporary copy of a file'''
  1095. tmpfile = Utils.create_tmpfile('lib_openshift-')
  1096. Utils._write(tmpfile, open(inc_file).read())
  1097. # Cleanup the tmpfile
  1098. atexit.register(Utils.cleanup, [tmpfile])
  1099. return tmpfile
  1100. @staticmethod
  1101. def create_tmpfile(prefix='tmp'):
  1102. ''' Generates and returns a temporary file name '''
  1103. with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
  1104. return tmp.name
  1105. @staticmethod
  1106. def create_tmp_files_from_contents(content, content_type=None):
  1107. '''Turn an array of dict: filename, content into a files array'''
  1108. if not isinstance(content, list):
  1109. content = [content]
  1110. files = []
  1111. for item in content:
  1112. path = Utils.create_tmp_file_from_contents(item['path'] + '-',
  1113. item['data'],
  1114. ftype=content_type)
  1115. files.append({'name': os.path.basename(item['path']),
  1116. 'path': path})
  1117. return files
  1118. @staticmethod
  1119. def cleanup(files):
  1120. '''Clean up on exit '''
  1121. for sfile in files:
  1122. if os.path.exists(sfile):
  1123. if os.path.isdir(sfile):
  1124. shutil.rmtree(sfile)
  1125. elif os.path.isfile(sfile):
  1126. os.remove(sfile)
  1127. @staticmethod
  1128. def exists(results, _name):
  1129. ''' Check to see if the results include the name '''
  1130. if not results:
  1131. return False
  1132. if Utils.find_result(results, _name):
  1133. return True
  1134. return False
  1135. @staticmethod
  1136. def find_result(results, _name):
  1137. ''' Find the specified result by name'''
  1138. rval = None
  1139. for result in results:
  1140. if 'metadata' in result and result['metadata']['name'] == _name:
  1141. rval = result
  1142. break
  1143. return rval
  1144. @staticmethod
  1145. def get_resource_file(sfile, sfile_type='yaml'):
  1146. ''' return the service file '''
  1147. contents = None
  1148. with open(sfile) as sfd:
  1149. contents = sfd.read()
  1150. if sfile_type == 'yaml':
  1151. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  1152. # pylint: disable=no-member
  1153. if hasattr(yaml, 'RoundTripLoader'):
  1154. contents = yaml.load(contents, yaml.RoundTripLoader)
  1155. else:
  1156. contents = yaml.safe_load(contents)
  1157. elif sfile_type == 'json':
  1158. contents = json.loads(contents)
  1159. return contents
  1160. @staticmethod
  1161. def filter_versions(stdout):
  1162. ''' filter the oc version output '''
  1163. version_dict = {}
  1164. version_search = ['oc', 'openshift', 'kubernetes']
  1165. for line in stdout.strip().split('\n'):
  1166. for term in version_search:
  1167. if not line:
  1168. continue
  1169. if line.startswith(term):
  1170. version_dict[term] = line.split()[-1]
  1171. # horrible hack to get openshift version in Openshift 3.2
  1172. # By default "oc version in 3.2 does not return an "openshift" version
  1173. if "openshift" not in version_dict:
  1174. version_dict["openshift"] = version_dict["oc"]
  1175. return version_dict
  1176. @staticmethod
  1177. def add_custom_versions(versions):
  1178. ''' create custom versions strings '''
  1179. versions_dict = {}
  1180. for tech, version in versions.items():
  1181. # clean up "-" from version
  1182. if "-" in version:
  1183. version = version.split("-")[0]
  1184. if version.startswith('v'):
  1185. version = version[1:] # Remove the 'v' prefix
  1186. versions_dict[tech + '_numeric'] = version.split('+')[0]
  1187. # "3.3.0.33" is what we have, we want "3.3"
  1188. versions_dict[tech + '_short'] = "{}.{}".format(*version.split('.'))
  1189. return versions_dict
  1190. @staticmethod
  1191. def openshift_installed():
  1192. ''' check if openshift is installed '''
  1193. import rpm
  1194. transaction_set = rpm.TransactionSet()
  1195. rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
  1196. return rpmquery.count() > 0
  1197. # Disabling too-many-branches. This is a yaml dictionary comparison function
  1198. # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
  1199. @staticmethod
  1200. def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
  1201. ''' Given a user defined definition, compare it with the results given back by our query. '''
  1202. # Currently these values are autogenerated and we do not need to check them
  1203. skip = ['metadata', 'status']
  1204. if skip_keys:
  1205. skip.extend(skip_keys)
  1206. for key, value in result_def.items():
  1207. if key in skip:
  1208. continue
  1209. # Both are lists
  1210. if isinstance(value, list):
  1211. if key not in user_def:
  1212. if debug:
  1213. print('User data does not have key [%s]' % key)
  1214. print('User data: %s' % user_def)
  1215. return False
  1216. if not isinstance(user_def[key], list):
  1217. if debug:
  1218. print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
  1219. return False
  1220. if len(user_def[key]) != len(value):
  1221. if debug:
  1222. print("List lengths are not equal.")
  1223. print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
  1224. print("user_def: %s" % user_def[key])
  1225. print("value: %s" % value)
  1226. return False
  1227. for values in zip(user_def[key], value):
  1228. if isinstance(values[0], dict) and isinstance(values[1], dict):
  1229. if debug:
  1230. print('sending list - list')
  1231. print(type(values[0]))
  1232. print(type(values[1]))
  1233. result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
  1234. if not result:
  1235. print('list compare returned false')
  1236. return False
  1237. elif value != user_def[key]:
  1238. if debug:
  1239. print('value should be identical')
  1240. print(user_def[key])
  1241. print(value)
  1242. return False
  1243. # recurse on a dictionary
  1244. elif isinstance(value, dict):
  1245. if key not in user_def:
  1246. if debug:
  1247. print("user_def does not have key [%s]" % key)
  1248. return False
  1249. if not isinstance(user_def[key], dict):
  1250. if debug:
  1251. print("dict returned false: not instance of dict")
  1252. return False
  1253. # before passing ensure keys match
  1254. api_values = set(value.keys()) - set(skip)
  1255. user_values = set(user_def[key].keys()) - set(skip)
  1256. if api_values != user_values:
  1257. if debug:
  1258. print("keys are not equal in dict")
  1259. print(user_values)
  1260. print(api_values)
  1261. return False
  1262. result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
  1263. if not result:
  1264. if debug:
  1265. print("dict returned false")
  1266. print(result)
  1267. return False
  1268. # Verify each key, value pair is the same
  1269. else:
  1270. if key not in user_def or value != user_def[key]:
  1271. if debug:
  1272. print("value not equal; user_def does not have key")
  1273. print(key)
  1274. print(value)
  1275. if key in user_def:
  1276. print(user_def[key])
  1277. return False
  1278. if debug:
  1279. print('returning true')
  1280. return True
  1281. class OpenShiftCLIConfig(object):
  1282. '''Generic Config'''
  1283. def __init__(self, rname, namespace, kubeconfig, options):
  1284. self.kubeconfig = kubeconfig
  1285. self.name = rname
  1286. self.namespace = namespace
  1287. self._options = options
  1288. @property
  1289. def config_options(self):
  1290. ''' return config options '''
  1291. return self._options
  1292. def to_option_list(self, ascommalist=''):
  1293. '''return all options as a string
  1294. if ascommalist is set to the name of a key, and
  1295. the value of that key is a dict, format the dict
  1296. as a list of comma delimited key=value pairs'''
  1297. return self.stringify(ascommalist)
  1298. def stringify(self, ascommalist=''):
  1299. ''' return the options hash as cli params in a string
  1300. if ascommalist is set to the name of a key, and
  1301. the value of that key is a dict, format the dict
  1302. as a list of comma delimited key=value pairs '''
  1303. rval = []
  1304. for key in sorted(self.config_options.keys()):
  1305. data = self.config_options[key]
  1306. if data['include'] \
  1307. and (data['value'] is not None or isinstance(data['value'], int)):
  1308. if key == ascommalist:
  1309. val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
  1310. else:
  1311. val = data['value']
  1312. rval.append('--{}={}'.format(key.replace('_', '-'), val))
  1313. return rval
  1314. # -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
  1315. # -*- -*- -*- Begin included fragment: lib/service.py -*- -*- -*-
  1316. # pylint: disable=too-many-instance-attributes
  1317. class ServiceConfig(object):
  1318. ''' Handle service options '''
  1319. # pylint: disable=too-many-arguments
  1320. def __init__(self,
  1321. sname,
  1322. namespace,
  1323. ports,
  1324. annotations=None,
  1325. selector=None,
  1326. labels=None,
  1327. cluster_ip=None,
  1328. portal_ip=None,
  1329. session_affinity=None,
  1330. service_type=None,
  1331. external_ips=None):
  1332. ''' constructor for handling service options '''
  1333. self.name = sname
  1334. self.namespace = namespace
  1335. self.ports = ports
  1336. self.annotations = annotations
  1337. self.selector = selector
  1338. self.labels = labels
  1339. self.cluster_ip = cluster_ip
  1340. self.portal_ip = portal_ip
  1341. self.session_affinity = session_affinity
  1342. self.service_type = service_type
  1343. self.external_ips = external_ips
  1344. self.data = {}
  1345. self.create_dict()
  1346. def create_dict(self):
  1347. ''' instantiates a service dict '''
  1348. self.data['apiVersion'] = 'v1'
  1349. self.data['kind'] = 'Service'
  1350. self.data['metadata'] = {}
  1351. self.data['metadata']['name'] = self.name
  1352. self.data['metadata']['namespace'] = self.namespace
  1353. if self.labels:
  1354. self.data['metadata']['labels'] = {}
  1355. for lab, lab_value in self.labels.items():
  1356. self.data['metadata']['labels'][lab] = lab_value
  1357. if self.annotations:
  1358. self.data['metadata']['annotations'] = self.annotations
  1359. self.data['spec'] = {}
  1360. if self.ports:
  1361. self.data['spec']['ports'] = self.ports
  1362. else:
  1363. self.data['spec']['ports'] = []
  1364. if self.selector:
  1365. self.data['spec']['selector'] = self.selector
  1366. self.data['spec']['sessionAffinity'] = self.session_affinity or 'None'
  1367. if self.cluster_ip:
  1368. self.data['spec']['clusterIP'] = self.cluster_ip
  1369. if self.portal_ip:
  1370. self.data['spec']['portalIP'] = self.portal_ip
  1371. if self.service_type:
  1372. self.data['spec']['type'] = self.service_type
  1373. if self.external_ips:
  1374. self.data['spec']['externalIPs'] = self.external_ips
  1375. # pylint: disable=too-many-instance-attributes,too-many-public-methods
  1376. class Service(Yedit):
  1377. ''' Class to model the oc service object '''
  1378. port_path = "spec.ports"
  1379. portal_ip = "spec.portalIP"
  1380. cluster_ip = "spec.clusterIP"
  1381. selector_path = 'spec.selector'
  1382. kind = 'Service'
  1383. external_ips = "spec.externalIPs"
  1384. def __init__(self, content):
  1385. '''Service constructor'''
  1386. super(Service, self).__init__(content=content)
  1387. def get_ports(self):
  1388. ''' get a list of ports '''
  1389. return self.get(Service.port_path) or []
  1390. def get_selector(self):
  1391. ''' get the service selector'''
  1392. return self.get(Service.selector_path) or {}
  1393. def add_ports(self, inc_ports):
  1394. ''' add a port object to the ports list '''
  1395. if not isinstance(inc_ports, list):
  1396. inc_ports = [inc_ports]
  1397. ports = self.get_ports()
  1398. if not ports:
  1399. self.put(Service.port_path, inc_ports)
  1400. else:
  1401. ports.extend(inc_ports)
  1402. return True
  1403. def find_ports(self, inc_port):
  1404. ''' find a specific port '''
  1405. for port in self.get_ports():
  1406. if port['port'] == inc_port['port']:
  1407. return port
  1408. return None
  1409. def delete_ports(self, inc_ports):
  1410. ''' remove a port from a service '''
  1411. if not isinstance(inc_ports, list):
  1412. inc_ports = [inc_ports]
  1413. ports = self.get(Service.port_path) or []
  1414. if not ports:
  1415. return True
  1416. removed = False
  1417. for inc_port in inc_ports:
  1418. port = self.find_ports(inc_port)
  1419. if port:
  1420. ports.remove(port)
  1421. removed = True
  1422. return removed
  1423. def add_cluster_ip(self, sip):
  1424. '''add cluster ip'''
  1425. self.put(Service.cluster_ip, sip)
  1426. def add_portal_ip(self, pip):
  1427. '''add cluster ip'''
  1428. self.put(Service.portal_ip, pip)
  1429. def get_external_ips(self):
  1430. ''' get a list of external_ips '''
  1431. return self.get(Service.external_ips) or []
  1432. def add_external_ips(self, inc_external_ips):
  1433. ''' add an external_ip to the external_ips list '''
  1434. if not isinstance(inc_external_ips, list):
  1435. inc_external_ips = [inc_external_ips]
  1436. external_ips = self.get_external_ips()
  1437. if not external_ips:
  1438. self.put(Service.external_ips, inc_external_ips)
  1439. else:
  1440. external_ips.extend(inc_external_ips)
  1441. return True
  1442. def find_external_ips(self, inc_external_ip):
  1443. ''' find a specific external IP '''
  1444. val = None
  1445. try:
  1446. idx = self.get_external_ips().index(inc_external_ip)
  1447. val = self.get_external_ips()[idx]
  1448. except ValueError:
  1449. pass
  1450. return val
  1451. def delete_external_ips(self, inc_external_ips):
  1452. ''' remove an external IP from a service '''
  1453. if not isinstance(inc_external_ips, list):
  1454. inc_external_ips = [inc_external_ips]
  1455. external_ips = self.get(Service.external_ips) or []
  1456. if not external_ips:
  1457. return True
  1458. removed = False
  1459. for inc_external_ip in inc_external_ips:
  1460. external_ip = self.find_external_ips(inc_external_ip)
  1461. if external_ip:
  1462. external_ips.remove(external_ip)
  1463. removed = True
  1464. return removed
  1465. # -*- -*- -*- End included fragment: lib/service.py -*- -*- -*-
  1466. # -*- -*- -*- Begin included fragment: lib/deploymentconfig.py -*- -*- -*-
  1467. # pylint: disable=too-many-public-methods
  1468. class DeploymentConfig(Yedit):
  1469. ''' Class to model an openshift DeploymentConfig'''
  1470. default_deployment_config = '''
  1471. apiVersion: v1
  1472. kind: DeploymentConfig
  1473. metadata:
  1474. name: default_dc
  1475. namespace: default
  1476. spec:
  1477. replicas: 0
  1478. selector:
  1479. default_dc: default_dc
  1480. strategy:
  1481. resources: {}
  1482. rollingParams:
  1483. intervalSeconds: 1
  1484. maxSurge: 0
  1485. maxUnavailable: 25%
  1486. timeoutSeconds: 600
  1487. updatePercent: -25
  1488. updatePeriodSeconds: 1
  1489. type: Rolling
  1490. template:
  1491. metadata:
  1492. spec:
  1493. containers:
  1494. - env:
  1495. - name: default
  1496. value: default
  1497. image: default
  1498. imagePullPolicy: IfNotPresent
  1499. name: default_dc
  1500. ports:
  1501. - containerPort: 8000
  1502. hostPort: 8000
  1503. protocol: TCP
  1504. name: default_port
  1505. resources: {}
  1506. terminationMessagePath: /dev/termination-log
  1507. dnsPolicy: ClusterFirst
  1508. hostNetwork: true
  1509. nodeSelector:
  1510. type: compute
  1511. restartPolicy: Always
  1512. securityContext: {}
  1513. serviceAccount: default
  1514. serviceAccountName: default
  1515. terminationGracePeriodSeconds: 30
  1516. triggers:
  1517. - type: ConfigChange
  1518. '''
  1519. replicas_path = "spec.replicas"
  1520. env_path = "spec.template.spec.containers[0].env"
  1521. volumes_path = "spec.template.spec.volumes"
  1522. container_path = "spec.template.spec.containers"
  1523. volume_mounts_path = "spec.template.spec.containers[0].volumeMounts"
  1524. def __init__(self, content=None):
  1525. ''' Constructor for deploymentconfig '''
  1526. if not content:
  1527. content = DeploymentConfig.default_deployment_config
  1528. super(DeploymentConfig, self).__init__(content=content)
  1529. def add_env_value(self, key, value):
  1530. ''' add key, value pair to env array '''
  1531. rval = False
  1532. env = self.get_env_vars()
  1533. if env:
  1534. env.append({'name': key, 'value': value})
  1535. rval = True
  1536. else:
  1537. result = self.put(DeploymentConfig.env_path, {'name': key, 'value': value})
  1538. rval = result[0]
  1539. return rval
  1540. def exists_env_value(self, key, value):
  1541. ''' return whether a key, value pair exists '''
  1542. results = self.get_env_vars()
  1543. if not results:
  1544. return False
  1545. for result in results:
  1546. if result['name'] == key:
  1547. if 'value' not in result:
  1548. if value == "" or value is None:
  1549. return True
  1550. elif result['value'] == value:
  1551. return True
  1552. return False
  1553. def exists_env_key(self, key):
  1554. ''' return whether a key, value pair exists '''
  1555. results = self.get_env_vars()
  1556. if not results:
  1557. return False
  1558. for result in results:
  1559. if result['name'] == key:
  1560. return True
  1561. return False
  1562. def get_env_var(self, key):
  1563. '''return a environment variables '''
  1564. results = self.get(DeploymentConfig.env_path) or []
  1565. if not results:
  1566. return None
  1567. for env_var in results:
  1568. if env_var['name'] == key:
  1569. return env_var
  1570. return None
  1571. def get_env_vars(self):
  1572. '''return a environment variables '''
  1573. return self.get(DeploymentConfig.env_path) or []
  1574. def delete_env_var(self, keys):
  1575. '''delete a list of keys '''
  1576. if not isinstance(keys, list):
  1577. keys = [keys]
  1578. env_vars_array = self.get_env_vars()
  1579. modified = False
  1580. idx = None
  1581. for key in keys:
  1582. for env_idx, env_var in enumerate(env_vars_array):
  1583. if env_var['name'] == key:
  1584. idx = env_idx
  1585. break
  1586. if idx:
  1587. modified = True
  1588. del env_vars_array[idx]
  1589. if modified:
  1590. return True
  1591. return False
  1592. def update_env_var(self, key, value):
  1593. '''place an env in the env var list'''
  1594. env_vars_array = self.get_env_vars()
  1595. idx = None
  1596. for env_idx, env_var in enumerate(env_vars_array):
  1597. if env_var['name'] == key:
  1598. idx = env_idx
  1599. break
  1600. if idx:
  1601. env_vars_array[idx]['value'] = value
  1602. else:
  1603. self.add_env_value(key, value)
  1604. return True
  1605. def exists_volume_mount(self, volume_mount):
  1606. ''' return whether a volume mount exists '''
  1607. exist_volume_mounts = self.get_volume_mounts()
  1608. if not exist_volume_mounts:
  1609. return False
  1610. volume_mount_found = False
  1611. for exist_volume_mount in exist_volume_mounts:
  1612. if exist_volume_mount['name'] == volume_mount['name']:
  1613. volume_mount_found = True
  1614. break
  1615. return volume_mount_found
  1616. def exists_volume(self, volume):
  1617. ''' return whether a volume exists '''
  1618. exist_volumes = self.get_volumes()
  1619. volume_found = False
  1620. for exist_volume in exist_volumes:
  1621. if exist_volume['name'] == volume['name']:
  1622. volume_found = True
  1623. break
  1624. return volume_found
  1625. def find_volume_by_name(self, volume, mounts=False):
  1626. ''' return the index of a volume '''
  1627. volumes = []
  1628. if mounts:
  1629. volumes = self.get_volume_mounts()
  1630. else:
  1631. volumes = self.get_volumes()
  1632. for exist_volume in volumes:
  1633. if exist_volume['name'] == volume['name']:
  1634. return exist_volume
  1635. return None
  1636. def get_replicas(self):
  1637. ''' return replicas setting '''
  1638. return self.get(DeploymentConfig.replicas_path)
  1639. def get_volume_mounts(self):
  1640. '''return volume mount information '''
  1641. return self.get_volumes(mounts=True)
  1642. def get_volumes(self, mounts=False):
  1643. '''return volume mount information '''
  1644. if mounts:
  1645. return self.get(DeploymentConfig.volume_mounts_path) or []
  1646. return self.get(DeploymentConfig.volumes_path) or []
  1647. def delete_volume_by_name(self, volume):
  1648. '''delete a volume '''
  1649. modified = False
  1650. exist_volume_mounts = self.get_volume_mounts()
  1651. exist_volumes = self.get_volumes()
  1652. del_idx = None
  1653. for idx, exist_volume in enumerate(exist_volumes):
  1654. if 'name' in exist_volume and exist_volume['name'] == volume['name']:
  1655. del_idx = idx
  1656. break
  1657. if del_idx != None:
  1658. del exist_volumes[del_idx]
  1659. modified = True
  1660. del_idx = None
  1661. for idx, exist_volume_mount in enumerate(exist_volume_mounts):
  1662. if 'name' in exist_volume_mount and exist_volume_mount['name'] == volume['name']:
  1663. del_idx = idx
  1664. break
  1665. if del_idx != None:
  1666. del exist_volume_mounts[idx]
  1667. modified = True
  1668. return modified
  1669. def add_volume_mount(self, volume_mount):
  1670. ''' add a volume or volume mount to the proper location '''
  1671. exist_volume_mounts = self.get_volume_mounts()
  1672. if not exist_volume_mounts and volume_mount:
  1673. self.put(DeploymentConfig.volume_mounts_path, [volume_mount])
  1674. else:
  1675. exist_volume_mounts.append(volume_mount)
  1676. def add_volume(self, volume):
  1677. ''' add a volume or volume mount to the proper location '''
  1678. exist_volumes = self.get_volumes()
  1679. if not volume:
  1680. return
  1681. if not exist_volumes:
  1682. self.put(DeploymentConfig.volumes_path, [volume])
  1683. else:
  1684. exist_volumes.append(volume)
  1685. def update_replicas(self, replicas):
  1686. ''' update replicas value '''
  1687. self.put(DeploymentConfig.replicas_path, replicas)
  1688. def update_volume(self, volume):
  1689. '''place an env in the env var list'''
  1690. exist_volumes = self.get_volumes()
  1691. if not volume:
  1692. return False
  1693. # update the volume
  1694. update_idx = None
  1695. for idx, exist_vol in enumerate(exist_volumes):
  1696. if exist_vol['name'] == volume['name']:
  1697. update_idx = idx
  1698. break
  1699. if update_idx != None:
  1700. exist_volumes[update_idx] = volume
  1701. else:
  1702. self.add_volume(volume)
  1703. return True
  1704. def update_volume_mount(self, volume_mount):
  1705. '''place an env in the env var list'''
  1706. modified = False
  1707. exist_volume_mounts = self.get_volume_mounts()
  1708. if not volume_mount:
  1709. return False
  1710. # update the volume mount
  1711. for exist_vol_mount in exist_volume_mounts:
  1712. if exist_vol_mount['name'] == volume_mount['name']:
  1713. if 'mountPath' in exist_vol_mount and \
  1714. str(exist_vol_mount['mountPath']) != str(volume_mount['mountPath']):
  1715. exist_vol_mount['mountPath'] = volume_mount['mountPath']
  1716. modified = True
  1717. break
  1718. if not modified:
  1719. self.add_volume_mount(volume_mount)
  1720. modified = True
  1721. return modified
  1722. def needs_update_volume(self, volume, volume_mount):
  1723. ''' verify a volume update is needed '''
  1724. exist_volume = self.find_volume_by_name(volume)
  1725. exist_volume_mount = self.find_volume_by_name(volume, mounts=True)
  1726. results = []
  1727. results.append(exist_volume['name'] == volume['name'])
  1728. if 'secret' in volume:
  1729. results.append('secret' in exist_volume)
  1730. results.append(exist_volume['secret']['secretName'] == volume['secret']['secretName'])
  1731. results.append(exist_volume_mount['name'] == volume_mount['name'])
  1732. results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
  1733. elif 'emptyDir' in volume:
  1734. results.append(exist_volume_mount['name'] == volume['name'])
  1735. results.append(exist_volume_mount['mountPath'] == volume_mount['mountPath'])
  1736. elif 'persistentVolumeClaim' in volume:
  1737. pvc = 'persistentVolumeClaim'
  1738. results.append(pvc in exist_volume)
  1739. if results[-1]:
  1740. results.append(exist_volume[pvc]['claimName'] == volume[pvc]['claimName'])
  1741. if 'claimSize' in volume[pvc]:
  1742. results.append(exist_volume[pvc]['claimSize'] == volume[pvc]['claimSize'])
  1743. elif 'hostpath' in volume:
  1744. results.append('hostPath' in exist_volume)
  1745. results.append(exist_volume['hostPath']['path'] == volume_mount['mountPath'])
  1746. return not all(results)
  1747. def needs_update_replicas(self, replicas):
  1748. ''' verify whether a replica update is needed '''
  1749. current_reps = self.get(DeploymentConfig.replicas_path)
  1750. return not current_reps == replicas
  1751. # -*- -*- -*- End included fragment: lib/deploymentconfig.py -*- -*- -*-
  1752. # -*- -*- -*- Begin included fragment: lib/serviceaccount.py -*- -*- -*-
  1753. class ServiceAccountConfig(object):
  1754. '''Service account config class
  1755. This class stores the options and returns a default service account
  1756. '''
  1757. # pylint: disable=too-many-arguments
  1758. def __init__(self, sname, namespace, kubeconfig, secrets=None, image_pull_secrets=None):
  1759. self.name = sname
  1760. self.kubeconfig = kubeconfig
  1761. self.namespace = namespace
  1762. self.secrets = secrets or []
  1763. self.image_pull_secrets = image_pull_secrets or []
  1764. self.data = {}
  1765. self.create_dict()
  1766. def create_dict(self):
  1767. ''' instantiate a properly structured volume '''
  1768. self.data['apiVersion'] = 'v1'
  1769. self.data['kind'] = 'ServiceAccount'
  1770. self.data['metadata'] = {}
  1771. self.data['metadata']['name'] = self.name
  1772. self.data['metadata']['namespace'] = self.namespace
  1773. self.data['secrets'] = []
  1774. if self.secrets:
  1775. for sec in self.secrets:
  1776. self.data['secrets'].append({"name": sec})
  1777. self.data['imagePullSecrets'] = []
  1778. if self.image_pull_secrets:
  1779. for sec in self.image_pull_secrets:
  1780. self.data['imagePullSecrets'].append({"name": sec})
  1781. class ServiceAccount(Yedit):
  1782. ''' Class to wrap the oc command line tools '''
  1783. image_pull_secrets_path = "imagePullSecrets"
  1784. secrets_path = "secrets"
  1785. def __init__(self, content):
  1786. '''ServiceAccount constructor'''
  1787. super(ServiceAccount, self).__init__(content=content)
  1788. self._secrets = None
  1789. self._image_pull_secrets = None
  1790. @property
  1791. def image_pull_secrets(self):
  1792. ''' property for image_pull_secrets '''
  1793. if self._image_pull_secrets is None:
  1794. self._image_pull_secrets = self.get(ServiceAccount.image_pull_secrets_path) or []
  1795. return self._image_pull_secrets
  1796. @image_pull_secrets.setter
  1797. def image_pull_secrets(self, secrets):
  1798. ''' property for secrets '''
  1799. self._image_pull_secrets = secrets
  1800. @property
  1801. def secrets(self):
  1802. ''' property for secrets '''
  1803. if not self._secrets:
  1804. self._secrets = self.get(ServiceAccount.secrets_path) or []
  1805. return self._secrets
  1806. @secrets.setter
  1807. def secrets(self, secrets):
  1808. ''' property for secrets '''
  1809. self._secrets = secrets
  1810. def delete_secret(self, inc_secret):
  1811. ''' remove a secret '''
  1812. remove_idx = None
  1813. for idx, sec in enumerate(self.secrets):
  1814. if sec['name'] == inc_secret:
  1815. remove_idx = idx
  1816. break
  1817. if remove_idx:
  1818. del self.secrets[remove_idx]
  1819. return True
  1820. return False
  1821. def delete_image_pull_secret(self, inc_secret):
  1822. ''' remove a image_pull_secret '''
  1823. remove_idx = None
  1824. for idx, sec in enumerate(self.image_pull_secrets):
  1825. if sec['name'] == inc_secret:
  1826. remove_idx = idx
  1827. break
  1828. if remove_idx:
  1829. del self.image_pull_secrets[remove_idx]
  1830. return True
  1831. return False
  1832. def find_secret(self, inc_secret):
  1833. '''find secret'''
  1834. for secret in self.secrets:
  1835. if secret['name'] == inc_secret:
  1836. return secret
  1837. return None
  1838. def find_image_pull_secret(self, inc_secret):
  1839. '''find secret'''
  1840. for secret in self.image_pull_secrets:
  1841. if secret['name'] == inc_secret:
  1842. return secret
  1843. return None
  1844. def add_secret(self, inc_secret):
  1845. '''add secret'''
  1846. if self.secrets:
  1847. self.secrets.append({"name": inc_secret}) # pylint: disable=no-member
  1848. else:
  1849. self.put(ServiceAccount.secrets_path, [{"name": inc_secret}])
  1850. def add_image_pull_secret(self, inc_secret):
  1851. '''add image_pull_secret'''
  1852. if self.image_pull_secrets:
  1853. self.image_pull_secrets.append({"name": inc_secret}) # pylint: disable=no-member
  1854. else:
  1855. self.put(ServiceAccount.image_pull_secrets_path, [{"name": inc_secret}])
  1856. # -*- -*- -*- End included fragment: lib/serviceaccount.py -*- -*- -*-
  1857. # -*- -*- -*- Begin included fragment: lib/secret.py -*- -*- -*-
  1858. # pylint: disable=too-many-instance-attributes
  1859. class SecretConfig(object):
  1860. ''' Handle secret options '''
  1861. # pylint: disable=too-many-arguments
  1862. def __init__(self,
  1863. sname,
  1864. namespace,
  1865. kubeconfig,
  1866. secrets=None,
  1867. stype=None,
  1868. annotations=None):
  1869. ''' constructor for handling secret options '''
  1870. self.kubeconfig = kubeconfig
  1871. self.name = sname
  1872. self.type = stype
  1873. self.namespace = namespace
  1874. self.secrets = secrets
  1875. self.annotations = annotations
  1876. self.data = {}
  1877. self.create_dict()
  1878. def create_dict(self):
  1879. ''' assign the correct properties for a secret dict '''
  1880. self.data['apiVersion'] = 'v1'
  1881. self.data['kind'] = 'Secret'
  1882. self.data['type'] = self.type
  1883. self.data['metadata'] = {}
  1884. self.data['metadata']['name'] = self.name
  1885. self.data['metadata']['namespace'] = self.namespace
  1886. self.data['data'] = {}
  1887. if self.secrets:
  1888. for key, value in self.secrets.items():
  1889. self.data['data'][key] = value
  1890. if self.annotations:
  1891. self.data['metadata']['annotations'] = self.annotations
  1892. # pylint: disable=too-many-instance-attributes
  1893. class Secret(Yedit):
  1894. ''' Class to wrap the oc command line tools '''
  1895. secret_path = "data"
  1896. kind = 'secret'
  1897. def __init__(self, content):
  1898. '''secret constructor'''
  1899. super(Secret, self).__init__(content=content)
  1900. self._secrets = None
  1901. @property
  1902. def secrets(self):
  1903. '''secret property getter'''
  1904. if self._secrets is None:
  1905. self._secrets = self.get_secrets()
  1906. return self._secrets
  1907. @secrets.setter
  1908. def secrets(self):
  1909. '''secret property setter'''
  1910. if self._secrets is None:
  1911. self._secrets = self.get_secrets()
  1912. return self._secrets
  1913. def get_secrets(self):
  1914. ''' returns all of the defined secrets '''
  1915. return self.get(Secret.secret_path) or {}
  1916. def add_secret(self, key, value):
  1917. ''' add a secret '''
  1918. if self.secrets:
  1919. self.secrets[key] = value
  1920. else:
  1921. self.put(Secret.secret_path, {key: value})
  1922. return True
  1923. def delete_secret(self, key):
  1924. ''' delete secret'''
  1925. try:
  1926. del self.secrets[key]
  1927. except KeyError as _:
  1928. return False
  1929. return True
  1930. def find_secret(self, key):
  1931. ''' find secret'''
  1932. rval = None
  1933. try:
  1934. rval = self.secrets[key]
  1935. except KeyError as _:
  1936. return None
  1937. return {'key': key, 'value': rval}
  1938. def update_secret(self, key, value):
  1939. ''' update a secret'''
  1940. if key in self.secrets:
  1941. self.secrets[key] = value
  1942. else:
  1943. self.add_secret(key, value)
  1944. return True
  1945. # -*- -*- -*- End included fragment: lib/secret.py -*- -*- -*-
  1946. # -*- -*- -*- Begin included fragment: lib/rolebinding.py -*- -*- -*-
  1947. # pylint: disable=too-many-instance-attributes
  1948. class RoleBindingConfig(object):
  1949. ''' Handle rolebinding config '''
  1950. # pylint: disable=too-many-arguments
  1951. def __init__(self,
  1952. name,
  1953. namespace,
  1954. kubeconfig,
  1955. group_names=None,
  1956. role_ref=None,
  1957. subjects=None,
  1958. usernames=None):
  1959. ''' constructor for handling rolebinding options '''
  1960. self.kubeconfig = kubeconfig
  1961. self.name = name
  1962. self.namespace = namespace
  1963. self.group_names = group_names
  1964. self.role_ref = role_ref
  1965. self.subjects = subjects
  1966. self.usernames = usernames
  1967. self.data = {}
  1968. self.create_dict()
  1969. def create_dict(self):
  1970. ''' create a default rolebinding as a dict '''
  1971. self.data['apiVersion'] = 'v1'
  1972. self.data['kind'] = 'RoleBinding'
  1973. self.data['groupNames'] = self.group_names
  1974. self.data['metadata']['name'] = self.name
  1975. self.data['metadata']['namespace'] = self.namespace
  1976. self.data['roleRef'] = self.role_ref
  1977. self.data['subjects'] = self.subjects
  1978. self.data['userNames'] = self.usernames
  1979. # pylint: disable=too-many-instance-attributes,too-many-public-methods
  1980. class RoleBinding(Yedit):
  1981. ''' Class to model a rolebinding openshift object'''
  1982. group_names_path = "groupNames"
  1983. role_ref_path = "roleRef"
  1984. subjects_path = "subjects"
  1985. user_names_path = "userNames"
  1986. kind = 'RoleBinding'
  1987. def __init__(self, content):
  1988. '''RoleBinding constructor'''
  1989. super(RoleBinding, self).__init__(content=content)
  1990. self._subjects = None
  1991. self._role_ref = None
  1992. self._group_names = None
  1993. self._user_names = None
  1994. @property
  1995. def subjects(self):
  1996. ''' subjects property '''
  1997. if self._subjects is None:
  1998. self._subjects = self.get_subjects()
  1999. return self._subjects
  2000. @subjects.setter
  2001. def subjects(self, data):
  2002. ''' subjects property setter'''
  2003. self._subjects = data
  2004. @property
  2005. def role_ref(self):
  2006. ''' role_ref property '''
  2007. if self._role_ref is None:
  2008. self._role_ref = self.get_role_ref()
  2009. return self._role_ref
  2010. @role_ref.setter
  2011. def role_ref(self, data):
  2012. ''' role_ref property setter'''
  2013. self._role_ref = data
  2014. @property
  2015. def group_names(self):
  2016. ''' group_names property '''
  2017. if self._group_names is None:
  2018. self._group_names = self.get_group_names()
  2019. return self._group_names
  2020. @group_names.setter
  2021. def group_names(self, data):
  2022. ''' group_names property setter'''
  2023. self._group_names = data
  2024. @property
  2025. def user_names(self):
  2026. ''' user_names property '''
  2027. if self._user_names is None:
  2028. self._user_names = self.get_user_names()
  2029. return self._user_names
  2030. @user_names.setter
  2031. def user_names(self, data):
  2032. ''' user_names property setter'''
  2033. self._user_names = data
  2034. def get_group_names(self):
  2035. ''' return groupNames '''
  2036. return self.get(RoleBinding.group_names_path) or []
  2037. def get_user_names(self):
  2038. ''' return usernames '''
  2039. return self.get(RoleBinding.user_names_path) or []
  2040. def get_role_ref(self):
  2041. ''' return role_ref '''
  2042. return self.get(RoleBinding.role_ref_path) or {}
  2043. def get_subjects(self):
  2044. ''' return subjects '''
  2045. return self.get(RoleBinding.subjects_path) or []
  2046. #### ADD #####
  2047. def add_subject(self, inc_subject):
  2048. ''' add a subject '''
  2049. if self.subjects:
  2050. # pylint: disable=no-member
  2051. self.subjects.append(inc_subject)
  2052. else:
  2053. self.put(RoleBinding.subjects_path, [inc_subject])
  2054. return True
  2055. def add_role_ref(self, inc_role_ref):
  2056. ''' add a role_ref '''
  2057. if not self.role_ref:
  2058. self.put(RoleBinding.role_ref_path, {"name": inc_role_ref})
  2059. return True
  2060. return False
  2061. def add_group_names(self, inc_group_names):
  2062. ''' add a group_names '''
  2063. if self.group_names:
  2064. # pylint: disable=no-member
  2065. self.group_names.append(inc_group_names)
  2066. else:
  2067. self.put(RoleBinding.group_names_path, [inc_group_names])
  2068. return True
  2069. def add_user_name(self, inc_user_name):
  2070. ''' add a username '''
  2071. if self.user_names:
  2072. # pylint: disable=no-member
  2073. self.user_names.append(inc_user_name)
  2074. else:
  2075. self.put(RoleBinding.user_names_path, [inc_user_name])
  2076. return True
  2077. #### /ADD #####
  2078. #### Remove #####
  2079. def remove_subject(self, inc_subject):
  2080. ''' remove a subject '''
  2081. try:
  2082. # pylint: disable=no-member
  2083. self.subjects.remove(inc_subject)
  2084. except ValueError as _:
  2085. return False
  2086. return True
  2087. def remove_role_ref(self, inc_role_ref):
  2088. ''' remove a role_ref '''
  2089. if self.role_ref and self.role_ref['name'] == inc_role_ref:
  2090. del self.role_ref['name']
  2091. return True
  2092. return False
  2093. def remove_group_name(self, inc_group_name):
  2094. ''' remove a groupname '''
  2095. try:
  2096. # pylint: disable=no-member
  2097. self.group_names.remove(inc_group_name)
  2098. except ValueError as _:
  2099. return False
  2100. return True
  2101. def remove_user_name(self, inc_user_name):
  2102. ''' remove a username '''
  2103. try:
  2104. # pylint: disable=no-member
  2105. self.user_names.remove(inc_user_name)
  2106. except ValueError as _:
  2107. return False
  2108. return True
  2109. #### /REMOVE #####
  2110. #### UPDATE #####
  2111. def update_subject(self, inc_subject):
  2112. ''' update a subject '''
  2113. try:
  2114. # pylint: disable=no-member
  2115. index = self.subjects.index(inc_subject)
  2116. except ValueError as _:
  2117. return self.add_subject(inc_subject)
  2118. self.subjects[index] = inc_subject
  2119. return True
  2120. def update_group_name(self, inc_group_name):
  2121. ''' update a groupname '''
  2122. try:
  2123. # pylint: disable=no-member
  2124. index = self.group_names.index(inc_group_name)
  2125. except ValueError as _:
  2126. return self.add_group_names(inc_group_name)
  2127. self.group_names[index] = inc_group_name
  2128. return True
  2129. def update_user_name(self, inc_user_name):
  2130. ''' update a username '''
  2131. try:
  2132. # pylint: disable=no-member
  2133. index = self.user_names.index(inc_user_name)
  2134. except ValueError as _:
  2135. return self.add_user_name(inc_user_name)
  2136. self.user_names[index] = inc_user_name
  2137. return True
  2138. def update_role_ref(self, inc_role_ref):
  2139. ''' update a role_ref '''
  2140. self.role_ref['name'] = inc_role_ref
  2141. return True
  2142. #### /UPDATE #####
  2143. #### FIND ####
  2144. def find_subject(self, inc_subject):
  2145. ''' find a subject '''
  2146. index = None
  2147. try:
  2148. # pylint: disable=no-member
  2149. index = self.subjects.index(inc_subject)
  2150. except ValueError as _:
  2151. return index
  2152. return index
  2153. def find_group_name(self, inc_group_name):
  2154. ''' find a group_name '''
  2155. index = None
  2156. try:
  2157. # pylint: disable=no-member
  2158. index = self.group_names.index(inc_group_name)
  2159. except ValueError as _:
  2160. return index
  2161. return index
  2162. def find_user_name(self, inc_user_name):
  2163. ''' find a user_name '''
  2164. index = None
  2165. try:
  2166. # pylint: disable=no-member
  2167. index = self.user_names.index(inc_user_name)
  2168. except ValueError as _:
  2169. return index
  2170. return index
  2171. def find_role_ref(self, inc_role_ref):
  2172. ''' find a user_name '''
  2173. if self.role_ref and self.role_ref['name'] == inc_role_ref['name']:
  2174. return self.role_ref
  2175. return None
  2176. # -*- -*- -*- End included fragment: lib/rolebinding.py -*- -*- -*-
  2177. # -*- -*- -*- Begin included fragment: class/oc_adm_router.py -*- -*- -*-
  2178. class RouterException(Exception):
  2179. ''' Router exception'''
  2180. pass
  2181. class RouterConfig(OpenShiftCLIConfig):
  2182. ''' RouterConfig is a DTO for the router. '''
  2183. def __init__(self, rname, namespace, kubeconfig, router_options):
  2184. super(RouterConfig, self).__init__(rname, namespace, kubeconfig, router_options)
  2185. class Router(OpenShiftCLI):
  2186. ''' Class to wrap the oc command line tools '''
  2187. def __init__(self,
  2188. router_config,
  2189. verbose=False):
  2190. ''' Constructor for OpenshiftOC
  2191. a router consists of 3 or more parts
  2192. - dc/router
  2193. - svc/router
  2194. - sa/router
  2195. - secret/router-certs
  2196. - clusterrolebinding/router-router-role
  2197. '''
  2198. super(Router, self).__init__(router_config.namespace, router_config.kubeconfig, verbose)
  2199. self.config = router_config
  2200. self.verbose = verbose
  2201. self.router_parts = [{'kind': 'dc', 'name': self.config.name},
  2202. {'kind': 'svc', 'name': self.config.name},
  2203. {'kind': 'sa', 'name': self.config.config_options['service_account']['value']},
  2204. {'kind': 'secret', 'name': self.config.name + '-certs'},
  2205. {'kind': 'clusterrolebinding', 'name': 'router-' + self.config.name + '-role'},
  2206. ]
  2207. self.__prepared_router = None
  2208. self.dconfig = None
  2209. self.svc = None
  2210. self._secret = None
  2211. self._serviceaccount = None
  2212. self._rolebinding = None
  2213. @property
  2214. def prepared_router(self):
  2215. ''' property for the prepared router'''
  2216. if self.__prepared_router is None:
  2217. results = self._prepare_router()
  2218. if not results or 'returncode' in results and results['returncode'] != 0:
  2219. if 'stderr' in results:
  2220. raise RouterException('Could not perform router preparation: %s' % results['stderr'])
  2221. raise RouterException('Could not perform router preparation.')
  2222. self.__prepared_router = results
  2223. return self.__prepared_router
  2224. @prepared_router.setter
  2225. def prepared_router(self, obj):
  2226. '''setter for the prepared_router'''
  2227. self.__prepared_router = obj
  2228. @property
  2229. def deploymentconfig(self):
  2230. ''' property deploymentconfig'''
  2231. return self.dconfig
  2232. @deploymentconfig.setter
  2233. def deploymentconfig(self, config):
  2234. ''' setter for property deploymentconfig '''
  2235. self.dconfig = config
  2236. @property
  2237. def service(self):
  2238. ''' property for service '''
  2239. return self.svc
  2240. @service.setter
  2241. def service(self, config):
  2242. ''' setter for property service '''
  2243. self.svc = config
  2244. @property
  2245. def secret(self):
  2246. ''' property secret '''
  2247. return self._secret
  2248. @secret.setter
  2249. def secret(self, config):
  2250. ''' setter for property secret '''
  2251. self._secret = config
  2252. @property
  2253. def serviceaccount(self):
  2254. ''' property for serviceaccount '''
  2255. return self._serviceaccount
  2256. @serviceaccount.setter
  2257. def serviceaccount(self, config):
  2258. ''' setter for property serviceaccount '''
  2259. self._serviceaccount = config
  2260. @property
  2261. def rolebinding(self):
  2262. ''' property rolebinding '''
  2263. return self._rolebinding
  2264. @rolebinding.setter
  2265. def rolebinding(self, config):
  2266. ''' setter for property rolebinding '''
  2267. self._rolebinding = config
  2268. def get_object_by_kind(self, kind):
  2269. '''return the current object kind by name'''
  2270. if re.match("^(dc|deploymentconfig)$", kind, flags=re.IGNORECASE):
  2271. return self.deploymentconfig
  2272. elif re.match("^(svc|service)$", kind, flags=re.IGNORECASE):
  2273. return self.service
  2274. elif re.match("^(sa|serviceaccount)$", kind, flags=re.IGNORECASE):
  2275. return self.serviceaccount
  2276. elif re.match("secret", kind, flags=re.IGNORECASE):
  2277. return self.secret
  2278. elif re.match("clusterrolebinding", kind, flags=re.IGNORECASE):
  2279. return self.rolebinding
  2280. return None
  2281. def get(self):
  2282. ''' return the self.router_parts '''
  2283. self.service = None
  2284. self.deploymentconfig = None
  2285. self.serviceaccount = None
  2286. self.secret = None
  2287. self.rolebinding = None
  2288. for part in self.router_parts:
  2289. result = self._get(part['kind'], name=part['name'])
  2290. if result['returncode'] == 0 and part['kind'] == 'dc':
  2291. self.deploymentconfig = DeploymentConfig(result['results'][0])
  2292. elif result['returncode'] == 0 and part['kind'] == 'svc':
  2293. self.service = Service(content=result['results'][0])
  2294. elif result['returncode'] == 0 and part['kind'] == 'sa':
  2295. self.serviceaccount = ServiceAccount(content=result['results'][0])
  2296. elif result['returncode'] == 0 and part['kind'] == 'secret':
  2297. self.secret = Secret(content=result['results'][0])
  2298. elif result['returncode'] == 0 and part['kind'] == 'clusterrolebinding':
  2299. self.rolebinding = RoleBinding(content=result['results'][0])
  2300. return {'deploymentconfig': self.deploymentconfig,
  2301. 'service': self.service,
  2302. 'serviceaccount': self.serviceaccount,
  2303. 'secret': self.secret,
  2304. 'clusterrolebinding': self.rolebinding,
  2305. }
  2306. def exists(self):
  2307. '''return a whether svc or dc exists '''
  2308. if self.deploymentconfig and self.service and self.secret and self.serviceaccount:
  2309. return True
  2310. return False
  2311. def delete(self):
  2312. '''return all pods '''
  2313. parts = []
  2314. for part in self.router_parts:
  2315. parts.append(self._delete(part['kind'], part['name']))
  2316. rval = 0
  2317. for part in parts:
  2318. if part['returncode'] != 0 and not 'already exist' in part['stderr']:
  2319. rval = part['returncode']
  2320. return {'returncode': rval, 'results': parts}
  2321. def add_modifications(self, deploymentconfig):
  2322. '''modify the deployment config'''
  2323. # We want modifications in the form of edits coming in from the module.
  2324. # Let's apply these here
  2325. # If extended validation is enabled, set the corresponding environment
  2326. # variable.
  2327. if self.config.config_options['extended_validation']['value']:
  2328. if not deploymentconfig.exists_env_key('EXTENDED_VALIDATION'):
  2329. deploymentconfig.add_env_value('EXTENDED_VALIDATION', "true")
  2330. else:
  2331. deploymentconfig.update_env_var('EXTENDED_VALIDATION', "true")
  2332. # Apply any edits.
  2333. edit_results = []
  2334. for edit in self.config.config_options['edits'].get('value', []):
  2335. if edit['action'] == 'put':
  2336. edit_results.append(deploymentconfig.put(edit['key'],
  2337. edit['value']))
  2338. if edit['action'] == 'update':
  2339. edit_results.append(deploymentconfig.update(edit['key'],
  2340. edit['value'],
  2341. edit.get('index', None),
  2342. edit.get('curr_value', None)))
  2343. if edit['action'] == 'append':
  2344. edit_results.append(deploymentconfig.append(edit['key'],
  2345. edit['value']))
  2346. if edit_results and not any([res[0] for res in edit_results]):
  2347. return None
  2348. return deploymentconfig
  2349. # pylint: disable=too-many-branches
  2350. def _prepare_router(self):
  2351. '''prepare router for instantiation'''
  2352. # if cacert, key, and cert were passed, combine them into a pem file
  2353. if (self.config.config_options['cacert_file']['value'] and
  2354. self.config.config_options['cert_file']['value'] and
  2355. self.config.config_options['key_file']['value']):
  2356. router_pem = '/tmp/router.pem'
  2357. with open(router_pem, 'w') as rfd:
  2358. rfd.write(open(self.config.config_options['cert_file']['value']).read())
  2359. rfd.write(open(self.config.config_options['key_file']['value']).read())
  2360. if self.config.config_options['cacert_file']['value'] and \
  2361. os.path.exists(self.config.config_options['cacert_file']['value']):
  2362. rfd.write(open(self.config.config_options['cacert_file']['value']).read())
  2363. atexit.register(Utils.cleanup, [router_pem])
  2364. self.config.config_options['default_cert']['value'] = router_pem
  2365. elif self.config.config_options['default_cert']['value'] is None:
  2366. # No certificate was passed to us. do not pass one to oc adm router
  2367. self.config.config_options['default_cert']['include'] = False
  2368. options = self.config.to_option_list(ascommalist='labels')
  2369. cmd = ['router', self.config.name]
  2370. cmd.extend(options)
  2371. cmd.extend(['--dry-run=True', '-o', 'json'])
  2372. results = self.openshift_cmd(cmd, oadm=True, output=True, output_type='json')
  2373. # pylint: disable=maybe-no-member
  2374. if results['returncode'] != 0 or 'items' not in results['results']:
  2375. return results
  2376. oc_objects = {'DeploymentConfig': {'obj': None, 'path': None, 'update': False},
  2377. 'Secret': {'obj': None, 'path': None, 'update': False},
  2378. 'ServiceAccount': {'obj': None, 'path': None, 'update': False},
  2379. 'ClusterRoleBinding': {'obj': None, 'path': None, 'update': False},
  2380. 'Service': {'obj': None, 'path': None, 'update': False},
  2381. }
  2382. # pylint: disable=invalid-sequence-index
  2383. for res in results['results']['items']:
  2384. if res['kind'] == 'DeploymentConfig':
  2385. oc_objects['DeploymentConfig']['obj'] = DeploymentConfig(res)
  2386. elif res['kind'] == 'Service':
  2387. oc_objects['Service']['obj'] = Service(res)
  2388. elif res['kind'] == 'ServiceAccount':
  2389. oc_objects['ServiceAccount']['obj'] = ServiceAccount(res)
  2390. elif res['kind'] == 'Secret':
  2391. oc_objects['Secret']['obj'] = Secret(res)
  2392. elif res['kind'] == 'ClusterRoleBinding':
  2393. oc_objects['ClusterRoleBinding']['obj'] = RoleBinding(res)
  2394. # Currently only deploymentconfig needs updating
  2395. # Verify we got a deploymentconfig
  2396. if not oc_objects['DeploymentConfig']['obj']:
  2397. return results
  2398. # add modifications added
  2399. oc_objects['DeploymentConfig']['obj'] = self.add_modifications(oc_objects['DeploymentConfig']['obj'])
  2400. for oc_type, oc_data in oc_objects.items():
  2401. if oc_data['obj'] is not None:
  2402. oc_data['path'] = Utils.create_tmp_file_from_contents(oc_type, oc_data['obj'].yaml_dict)
  2403. return oc_objects
  2404. def create(self):
  2405. '''Create a router
  2406. This includes the different parts:
  2407. - deploymentconfig
  2408. - service
  2409. - serviceaccount
  2410. - secrets
  2411. - clusterrolebinding
  2412. '''
  2413. results = []
  2414. self.needs_update()
  2415. # pylint: disable=maybe-no-member
  2416. for kind, oc_data in self.prepared_router.items():
  2417. if oc_data['obj'] is not None:
  2418. time.sleep(1)
  2419. if self.get_object_by_kind(kind) is None:
  2420. results.append(self._create(oc_data['path']))
  2421. elif oc_data['update']:
  2422. results.append(self._replace(oc_data['path']))
  2423. rval = 0
  2424. for result in results:
  2425. if result['returncode'] != 0 and not 'already exist' in result['stderr']:
  2426. rval = result['returncode']
  2427. return {'returncode': rval, 'results': results}
  2428. def update(self):
  2429. '''run update for the router. This performs a replace'''
  2430. results = []
  2431. # pylint: disable=maybe-no-member
  2432. for _, oc_data in self.prepared_router.items():
  2433. if oc_data['update']:
  2434. results.append(self._replace(oc_data['path']))
  2435. rval = 0
  2436. for result in results:
  2437. if result['returncode'] != 0:
  2438. rval = result['returncode']
  2439. return {'returncode': rval, 'results': results}
  2440. # pylint: disable=too-many-return-statements,too-many-branches
  2441. def needs_update(self):
  2442. ''' check to see if we need to update '''
  2443. # ServiceAccount:
  2444. # Need to determine changes from the pregenerated ones from the original
  2445. # Since these are auto generated, we can skip
  2446. skip = ['secrets', 'imagePullSecrets']
  2447. if self.serviceaccount is None or \
  2448. not Utils.check_def_equal(self.prepared_router['ServiceAccount']['obj'].yaml_dict,
  2449. self.serviceaccount.yaml_dict,
  2450. skip_keys=skip,
  2451. debug=self.verbose):
  2452. self.prepared_router['ServiceAccount']['update'] = True
  2453. # Secret:
  2454. # See if one was generated from our dry-run and verify it if needed
  2455. if self.prepared_router['Secret']['obj']:
  2456. if not self.secret:
  2457. self.prepared_router['Secret']['update'] = True
  2458. if self.secret is None or \
  2459. not Utils.check_def_equal(self.prepared_router['Secret']['obj'].yaml_dict,
  2460. self.secret.yaml_dict,
  2461. skip_keys=skip,
  2462. debug=self.verbose):
  2463. self.prepared_router['Secret']['update'] = True
  2464. # Service:
  2465. # Fix the ports to have protocol=TCP
  2466. for port in self.prepared_router['Service']['obj'].get('spec.ports'):
  2467. port['protocol'] = 'TCP'
  2468. skip = ['portalIP', 'clusterIP', 'sessionAffinity', 'type']
  2469. if self.service is None or \
  2470. not Utils.check_def_equal(self.prepared_router['Service']['obj'].yaml_dict,
  2471. self.service.yaml_dict,
  2472. skip_keys=skip,
  2473. debug=self.verbose):
  2474. self.prepared_router['Service']['update'] = True
  2475. # DeploymentConfig:
  2476. # Router needs some exceptions.
  2477. # We do not want to check the autogenerated password for stats admin
  2478. if self.deploymentconfig is not None:
  2479. if not self.config.config_options['stats_password']['value']:
  2480. for idx, env_var in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\
  2481. 'spec.template.spec.containers[0].env') or []):
  2482. if env_var['name'] == 'STATS_PASSWORD':
  2483. env_var['value'] = \
  2484. self.deploymentconfig.get('spec.template.spec.containers[0].env[%s].value' % idx)
  2485. break
  2486. # dry-run doesn't add the protocol to the ports section. We will manually do that.
  2487. for idx, port in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\
  2488. 'spec.template.spec.containers[0].ports') or []):
  2489. if not 'protocol' in port:
  2490. port['protocol'] = 'TCP'
  2491. # These are different when generating
  2492. skip = ['dnsPolicy',
  2493. 'terminationGracePeriodSeconds',
  2494. 'restartPolicy', 'timeoutSeconds',
  2495. 'livenessProbe', 'readinessProbe',
  2496. 'terminationMessagePath', 'hostPort',
  2497. 'defaultMode',
  2498. ]
  2499. if self.deploymentconfig is None or \
  2500. not Utils.check_def_equal(self.prepared_router['DeploymentConfig']['obj'].yaml_dict,
  2501. self.deploymentconfig.yaml_dict,
  2502. skip_keys=skip,
  2503. debug=self.verbose):
  2504. self.prepared_router['DeploymentConfig']['update'] = True
  2505. # Check if any of the parts need updating, if so, return True
  2506. # else, no need to update
  2507. # pylint: disable=no-member
  2508. return any([self.prepared_router[oc_type]['update'] for oc_type in self.prepared_router.keys()])
  2509. @staticmethod
  2510. def run_ansible(params, check_mode):
  2511. '''run the oc_adm_router module'''
  2512. rconfig = RouterConfig(params['name'],
  2513. params['namespace'],
  2514. params['kubeconfig'],
  2515. {'default_cert': {'value': params['default_cert'], 'include': True},
  2516. 'cert_file': {'value': params['cert_file'], 'include': False},
  2517. 'key_file': {'value': params['key_file'], 'include': False},
  2518. 'images': {'value': params['images'], 'include': True},
  2519. 'latest_images': {'value': params['latest_images'], 'include': True},
  2520. 'labels': {'value': params['labels'], 'include': True},
  2521. 'ports': {'value': ','.join(params['ports']), 'include': True},
  2522. 'replicas': {'value': params['replicas'], 'include': True},
  2523. 'selector': {'value': params['selector'], 'include': True},
  2524. 'service_account': {'value': params['service_account'], 'include': True},
  2525. 'router_type': {'value': params['router_type'], 'include': False},
  2526. 'host_network': {'value': params['host_network'], 'include': True},
  2527. 'extended_validation': {'value': params['extended_validation'], 'include': False},
  2528. 'external_host': {'value': params['external_host'], 'include': True},
  2529. 'external_host_vserver': {'value': params['external_host_vserver'],
  2530. 'include': True},
  2531. 'external_host_insecure': {'value': params['external_host_insecure'],
  2532. 'include': True},
  2533. 'external_host_partition_path': {'value': params['external_host_partition_path'],
  2534. 'include': True},
  2535. 'external_host_username': {'value': params['external_host_username'],
  2536. 'include': True},
  2537. 'external_host_password': {'value': params['external_host_password'],
  2538. 'include': True},
  2539. 'external_host_private_key': {'value': params['external_host_private_key'],
  2540. 'include': True},
  2541. 'stats_user': {'value': params['stats_user'], 'include': True},
  2542. 'stats_password': {'value': params['stats_password'], 'include': True},
  2543. 'stats_port': {'value': params['stats_port'], 'include': True},
  2544. # extra
  2545. 'cacert_file': {'value': params['cacert_file'], 'include': False},
  2546. # edits
  2547. 'edits': {'value': params['edits'], 'include': False},
  2548. })
  2549. state = params['state']
  2550. ocrouter = Router(rconfig, verbose=params['debug'])
  2551. api_rval = ocrouter.get()
  2552. ########
  2553. # get
  2554. ########
  2555. if state == 'list':
  2556. return {'changed': False, 'results': api_rval, 'state': state}
  2557. ########
  2558. # Delete
  2559. ########
  2560. if state == 'absent':
  2561. if not ocrouter.exists():
  2562. return {'changed': False, 'state': state}
  2563. if check_mode:
  2564. return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
  2565. # In case of delete we return a list of each object
  2566. # that represents a router and its result in a list
  2567. # pylint: disable=redefined-variable-type
  2568. api_rval = ocrouter.delete()
  2569. return {'changed': True, 'results': api_rval, 'state': state}
  2570. if state == 'present':
  2571. ########
  2572. # Create
  2573. ########
  2574. if not ocrouter.exists():
  2575. if check_mode:
  2576. return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
  2577. api_rval = ocrouter.create()
  2578. if api_rval['returncode'] != 0:
  2579. return {'failed': True, 'msg': api_rval}
  2580. return {'changed': True, 'results': api_rval, 'state': state}
  2581. ########
  2582. # Update
  2583. ########
  2584. if not ocrouter.needs_update():
  2585. return {'changed': False, 'state': state}
  2586. if check_mode:
  2587. return {'changed': False, 'msg': 'CHECK_MODE: Would have performed an update.'}
  2588. api_rval = ocrouter.update()
  2589. if api_rval['returncode'] != 0:
  2590. return {'failed': True, 'msg': api_rval}
  2591. return {'changed': True, 'results': api_rval, 'state': state}
  2592. # -*- -*- -*- End included fragment: class/oc_adm_router.py -*- -*- -*-
  2593. # -*- -*- -*- Begin included fragment: ansible/oc_adm_router.py -*- -*- -*-
  2594. def main():
  2595. '''
  2596. ansible oc module for router
  2597. '''
  2598. module = AnsibleModule(
  2599. argument_spec=dict(
  2600. state=dict(default='present', type='str',
  2601. choices=['present', 'absent']),
  2602. debug=dict(default=False, type='bool'),
  2603. namespace=dict(default='default', type='str'),
  2604. name=dict(default='router', type='str'),
  2605. kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
  2606. default_cert=dict(default=None, type='str'),
  2607. cert_file=dict(default=None, type='str'),
  2608. key_file=dict(default=None, type='str'),
  2609. images=dict(default=None, type='str'), #'registry.access.redhat.com/openshift3/ose-${component}:${version}'
  2610. latest_images=dict(default=False, type='bool'),
  2611. labels=dict(default=None, type='dict'),
  2612. ports=dict(default=['80:80', '443:443'], type='list'),
  2613. replicas=dict(default=1, type='int'),
  2614. selector=dict(default=None, type='str'),
  2615. service_account=dict(default='router', type='str'),
  2616. router_type=dict(default='haproxy-router', type='str'),
  2617. host_network=dict(default=True, type='bool'),
  2618. extended_validation=dict(default=True, type='bool'),
  2619. # external host options
  2620. external_host=dict(default=None, type='str'),
  2621. external_host_vserver=dict(default=None, type='str'),
  2622. external_host_insecure=dict(default=False, type='bool'),
  2623. external_host_partition_path=dict(default=None, type='str'),
  2624. external_host_username=dict(default=None, type='str'),
  2625. external_host_password=dict(default=None, type='str', no_log=True),
  2626. external_host_private_key=dict(default=None, type='str', no_log=True),
  2627. # Stats
  2628. stats_user=dict(default=None, type='str'),
  2629. stats_password=dict(default=None, type='str', no_log=True),
  2630. stats_port=dict(default=1936, type='int'),
  2631. # extra
  2632. cacert_file=dict(default=None, type='str'),
  2633. # edits
  2634. edits=dict(default=[], type='list'),
  2635. ),
  2636. mutually_exclusive=[["router_type", "images"],
  2637. ["key_file", "default_cert"],
  2638. ["cert_file", "default_cert"],
  2639. ["cacert_file", "default_cert"],
  2640. ],
  2641. required_together=[['cacert_file', 'cert_file', 'key_file']],
  2642. supports_check_mode=True,
  2643. )
  2644. results = Router.run_ansible(module.params, module.check_mode)
  2645. if 'failed' in results:
  2646. module.fail_json(**results)
  2647. module.exit_json(**results)
  2648. if __name__ == '__main__':
  2649. main()
  2650. # -*- -*- -*- End included fragment: ansible/oc_adm_router.py -*- -*- -*-