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. - State controls the action that will be taken with resource
  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: ["present", "absent", "list"]
  67. aliases: []
  68. kubeconfig:
  69. description:
  70. - The path for the kubeconfig file to use for authentication
  71. required: false
  72. default: /etc/origin/master/admin.kubeconfig
  73. aliases: []
  74. debug:
  75. description:
  76. - Turn on debug output.
  77. required: false
  78. default: False
  79. aliases: []
  80. name:
  81. description:
  82. - The name of the router
  83. required: false
  84. default: router
  85. aliases: []
  86. namespace:
  87. description:
  88. - The namespace where to manage the router.
  89. required: false
  90. default: default
  91. aliases: []
  92. images:
  93. description:
  94. - The image to base this router on - ${component} will be replaced with --type
  95. required: 'registry.redhat.io/openshift3/ose-${component}:${version}'
  96. default: None
  97. aliases: []
  98. latest_images:
  99. description:
  100. - If true, attempt to use the latest image for the registry instead of the latest release.
  101. required: false
  102. default: False
  103. aliases: []
  104. labels:
  105. description:
  106. - A set of labels to uniquely identify the registry and its components.
  107. required: false
  108. default: None
  109. aliases: []
  110. ports:
  111. description:
  112. - A list of strings in the 'port:port' format
  113. required: False
  114. default:
  115. - 80:80
  116. - 443:443
  117. aliases: []
  118. replicas:
  119. description:
  120. - The replication factor of the registry; commonly 2 when high availability is desired.
  121. required: False
  122. default: 1
  123. aliases: []
  124. selector:
  125. description:
  126. - Selector used to filter nodes on deployment. Used to run routers on a specific set of nodes.
  127. required: False
  128. default: None
  129. aliases: []
  130. service_account:
  131. description:
  132. - Name of the service account to use to run the router pod.
  133. required: False
  134. default: router
  135. aliases: []
  136. router_type:
  137. description:
  138. - The router image to use - if you specify --images this flag may be ignored.
  139. required: false
  140. default: haproxy-router
  141. aliases: []
  142. extended_validation:
  143. description:
  144. - If true, configure the router to perform extended validation on routes before admitting them.
  145. required: false
  146. default: True
  147. aliases: []
  148. external_host:
  149. description:
  150. - If the underlying router implementation connects with an external host, this is the external host's hostname.
  151. required: false
  152. default: None
  153. aliases: []
  154. external_host_vserver:
  155. description:
  156. - If the underlying router implementation uses virtual servers, this is the name of the virtual server for HTTP connections.
  157. required: false
  158. default: None
  159. aliases: []
  160. external_host_insecure:
  161. description:
  162. - If the underlying router implementation connects with an external host
  163. - over a secure connection, this causes the router to skip strict certificate verification with the external host.
  164. required: false
  165. default: False
  166. aliases: []
  167. external_host_partition_path:
  168. description:
  169. - If the underlying router implementation uses partitions for control boundaries, this is the path to use for that partition.
  170. required: false
  171. default: None
  172. aliases: []
  173. external_host_username:
  174. description:
  175. - If the underlying router implementation connects with an external host, this is the username for authenticating with the external host.
  176. required: false
  177. default: None
  178. aliases: []
  179. external_host_password:
  180. description:
  181. - If the underlying router implementation connects with an external host, this is the password for authenticating with the external host.
  182. required: false
  183. default: None
  184. aliases: []
  185. external_host_private_key:
  186. description:
  187. - If the underlying router implementation requires an SSH private key, this is the path to the private key file.
  188. required: false
  189. default: None
  190. aliases: []
  191. author:
  192. - "Kenny Woodson <kwoodson@redhat.com>"
  193. extends_documentation_fragment:
  194. - There are some exceptions to note when doing the idempotency in this module.
  195. - The strategy is to use the oc adm router command to generate a default
  196. - configuration when creating or updating a router. Often times there
  197. - differences from the generated template and what is in memory in openshift.
  198. - We make exceptions to not check these specific values when comparing objects.
  199. - Here are a list of exceptions:
  200. - - DeploymentConfig:
  201. - dnsPolicy
  202. - terminationGracePeriodSeconds
  203. - restartPolicy
  204. - timeoutSeconds
  205. - livenessProbe
  206. - readinessProbe
  207. - terminationMessagePath
  208. - hostPort
  209. - defaultMode
  210. - Service:
  211. - portalIP
  212. - clusterIP
  213. - sessionAffinity
  214. - type
  215. - ServiceAccount:
  216. - secrets
  217. - imagePullSecrets
  218. '''
  219. EXAMPLES = '''
  220. - name: create routers
  221. oc_adm_router:
  222. name: router
  223. service_account: router
  224. replicas: 2
  225. namespace: default
  226. selector: type=infra
  227. cert_file: /etc/origin/master/named_certificates/router.crt
  228. key_file: /etc/origin/master/named_certificates/router.key
  229. cacert_file: /etc/origin/master/named_certificates/router.ca
  230. edits:
  231. - key: spec.strategy.rollingParams
  232. value:
  233. intervalSeconds: 1
  234. maxSurge: 50%
  235. maxUnavailable: 50%
  236. timeoutSeconds: 600
  237. updatePeriodSeconds: 1
  238. action: put
  239. - key: spec.template.spec.containers[0].resources.limits.memory
  240. value: 2G
  241. action: put
  242. - key: spec.template.spec.containers[0].resources.requests.memory
  243. value: 1G
  244. action: put
  245. - key: spec.template.spec.containers[0].env
  246. value:
  247. name: ROUTER_MAX_CONNECTIONS
  248. value: "10000"
  249. action: update
  250. register: router_out
  251. run_once: True
  252. '''
  253. # -*- -*- -*- End included fragment: doc/router -*- -*- -*-
  254. # -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
  255. class YeditException(Exception): # pragma: no cover
  256. ''' Exception class for Yedit '''
  257. pass
  258. # pylint: disable=too-many-public-methods,too-many-instance-attributes
  259. class Yedit(object): # pragma: no cover
  260. ''' Class to modify yaml files '''
  261. re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
  262. re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
  263. com_sep = set(['.', '#', '|', ':'])
  264. # pylint: disable=too-many-arguments
  265. def __init__(self,
  266. filename=None,
  267. content=None,
  268. content_type='yaml',
  269. separator='.',
  270. backup_ext=None,
  271. backup=False):
  272. self.content = content
  273. self._separator = separator
  274. self.filename = filename
  275. self.__yaml_dict = content
  276. self.content_type = content_type
  277. self.backup = backup
  278. if backup_ext is None:
  279. self.backup_ext = ".{}".format(time.strftime("%Y%m%dT%H%M%S"))
  280. else:
  281. self.backup_ext = backup_ext
  282. self.load(content_type=self.content_type)
  283. if self.__yaml_dict is None:
  284. self.__yaml_dict = {}
  285. @property
  286. def separator(self):
  287. ''' getter method for separator '''
  288. return self._separator
  289. @separator.setter
  290. def separator(self, inc_sep):
  291. ''' setter method for separator '''
  292. self._separator = inc_sep
  293. @property
  294. def yaml_dict(self):
  295. ''' getter method for yaml_dict '''
  296. return self.__yaml_dict
  297. @yaml_dict.setter
  298. def yaml_dict(self, value):
  299. ''' setter method for yaml_dict '''
  300. self.__yaml_dict = value
  301. @staticmethod
  302. def parse_key(key, sep='.'):
  303. '''parse the key allowing the appropriate separator'''
  304. common_separators = list(Yedit.com_sep - set([sep]))
  305. return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
  306. @staticmethod
  307. def valid_key(key, sep='.'):
  308. '''validate the incoming key'''
  309. common_separators = list(Yedit.com_sep - set([sep]))
  310. if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
  311. return False
  312. return True
  313. # pylint: disable=too-many-return-statements,too-many-branches
  314. @staticmethod
  315. def remove_entry(data, key, index=None, value=None, sep='.'):
  316. ''' remove data at location key '''
  317. if key == '' and isinstance(data, dict):
  318. if value is not None:
  319. data.pop(value)
  320. elif index is not None:
  321. raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
  322. else:
  323. data.clear()
  324. return True
  325. elif key == '' and isinstance(data, list):
  326. ind = None
  327. if value is not None:
  328. try:
  329. ind = data.index(value)
  330. except ValueError:
  331. return False
  332. elif index is not None:
  333. ind = index
  334. else:
  335. del data[:]
  336. if ind is not None:
  337. data.pop(ind)
  338. return True
  339. if not (key and Yedit.valid_key(key, sep)) and \
  340. isinstance(data, (list, dict)):
  341. return None
  342. key_indexes = Yedit.parse_key(key, sep)
  343. for arr_ind, dict_key in key_indexes[:-1]:
  344. if dict_key and isinstance(data, dict):
  345. data = data.get(dict_key)
  346. elif (arr_ind and isinstance(data, list) and
  347. int(arr_ind) <= len(data) - 1):
  348. data = data[int(arr_ind)]
  349. else:
  350. return None
  351. # process last index for remove
  352. # expected list entry
  353. if key_indexes[-1][0]:
  354. if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  355. del data[int(key_indexes[-1][0])]
  356. return True
  357. # expected dict entry
  358. elif key_indexes[-1][1]:
  359. if isinstance(data, dict):
  360. del data[key_indexes[-1][1]]
  361. return True
  362. @staticmethod
  363. def add_entry(data, key, item=None, sep='.'):
  364. ''' Get an item from a dictionary with key notation a.b.c
  365. d = {'a': {'b': 'c'}}}
  366. key = a#b
  367. return c
  368. '''
  369. if key == '':
  370. pass
  371. elif (not (key and Yedit.valid_key(key, sep)) and
  372. isinstance(data, (list, dict))):
  373. return None
  374. key_indexes = Yedit.parse_key(key, sep)
  375. for arr_ind, dict_key in key_indexes[:-1]:
  376. if dict_key:
  377. if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
  378. data = data[dict_key]
  379. continue
  380. elif data and not isinstance(data, dict):
  381. raise YeditException("Unexpected item type found while going through key " +
  382. "path: {} (at key: {})".format(key, dict_key))
  383. data[dict_key] = {}
  384. data = data[dict_key]
  385. elif (arr_ind and isinstance(data, list) and
  386. int(arr_ind) <= len(data) - 1):
  387. data = data[int(arr_ind)]
  388. else:
  389. raise YeditException("Unexpected item type found while going through key path: {}".format(key))
  390. if key == '':
  391. data = item
  392. # process last index for add
  393. # expected list entry
  394. elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  395. data[int(key_indexes[-1][0])] = item
  396. # expected dict entry
  397. elif key_indexes[-1][1] and isinstance(data, dict):
  398. data[key_indexes[-1][1]] = item
  399. # didn't add/update to an existing list, nor add/update key to a dict
  400. # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
  401. # non-existent array
  402. else:
  403. raise YeditException("Error adding to object at path: {}".format(key))
  404. return data
  405. @staticmethod
  406. def get_entry(data, key, sep='.'):
  407. ''' Get an item from a dictionary with key notation a.b.c
  408. d = {'a': {'b': 'c'}}}
  409. key = a.b
  410. return c
  411. '''
  412. if key == '':
  413. pass
  414. elif (not (key and Yedit.valid_key(key, sep)) and
  415. isinstance(data, (list, dict))):
  416. return None
  417. key_indexes = Yedit.parse_key(key, sep)
  418. for arr_ind, dict_key in key_indexes:
  419. if dict_key and isinstance(data, dict):
  420. data = data.get(dict_key)
  421. elif (arr_ind and isinstance(data, list) and
  422. int(arr_ind) <= len(data) - 1):
  423. data = data[int(arr_ind)]
  424. else:
  425. return None
  426. return data
  427. @staticmethod
  428. def _write(filename, contents):
  429. ''' Actually write the file contents to disk. This helps with mocking. '''
  430. tmp_filename = filename + '.yedit'
  431. with open(tmp_filename, 'w') as yfd:
  432. fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  433. yfd.write(contents)
  434. fcntl.flock(yfd, fcntl.LOCK_UN)
  435. os.rename(tmp_filename, filename)
  436. def write(self):
  437. ''' write to file '''
  438. if not self.filename:
  439. raise YeditException('Please specify a filename.')
  440. if self.backup and self.file_exists():
  441. shutil.copy(self.filename, '{}{}'.format(self.filename, self.backup_ext))
  442. # Try to set format attributes if supported
  443. try:
  444. self.yaml_dict.fa.set_block_style()
  445. except AttributeError:
  446. pass
  447. # Try to use RoundTripDumper if supported.
  448. if self.content_type == 'yaml':
  449. try:
  450. Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
  451. except AttributeError:
  452. Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
  453. elif self.content_type == 'json':
  454. Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
  455. else:
  456. raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
  457. 'Please specify a content_type of yaml or json.')
  458. return (True, self.yaml_dict)
  459. def read(self):
  460. ''' read from file '''
  461. # check if it exists
  462. if self.filename is None or not self.file_exists():
  463. return None
  464. contents = None
  465. with open(self.filename) as yfd:
  466. contents = yfd.read()
  467. return contents
  468. def file_exists(self):
  469. ''' return whether file exists '''
  470. if os.path.exists(self.filename):
  471. return True
  472. return False
  473. def load(self, content_type='yaml'):
  474. ''' return yaml file '''
  475. contents = self.read()
  476. if not contents and not self.content:
  477. return None
  478. if self.content:
  479. if isinstance(self.content, dict):
  480. self.yaml_dict = self.content
  481. return self.yaml_dict
  482. elif isinstance(self.content, str):
  483. contents = self.content
  484. # check if it is yaml
  485. try:
  486. if content_type == 'yaml' and contents:
  487. # Try to set format attributes if supported
  488. try:
  489. self.yaml_dict.fa.set_block_style()
  490. except AttributeError:
  491. pass
  492. # Try to use RoundTripLoader if supported.
  493. try:
  494. self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
  495. except AttributeError:
  496. self.yaml_dict = yaml.safe_load(contents)
  497. # Try to set format attributes if supported
  498. try:
  499. self.yaml_dict.fa.set_block_style()
  500. except AttributeError:
  501. pass
  502. elif content_type == 'json' and contents:
  503. self.yaml_dict = json.loads(contents)
  504. except yaml.YAMLError as err:
  505. # Error loading yaml or json
  506. raise YeditException('Problem with loading yaml file. {}'.format(err))
  507. return self.yaml_dict
  508. def get(self, key):
  509. ''' get a specified key'''
  510. try:
  511. entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
  512. except KeyError:
  513. entry = None
  514. return entry
  515. def pop(self, path, key_or_item):
  516. ''' remove a key, value pair from a dict or an item for a list'''
  517. try:
  518. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  519. except KeyError:
  520. entry = None
  521. if entry is None:
  522. return (False, self.yaml_dict)
  523. if isinstance(entry, dict):
  524. # AUDIT:maybe-no-member makes sense due to fuzzy types
  525. # pylint: disable=maybe-no-member
  526. if key_or_item in entry:
  527. entry.pop(key_or_item)
  528. return (True, self.yaml_dict)
  529. return (False, self.yaml_dict)
  530. elif isinstance(entry, list):
  531. # AUDIT:maybe-no-member makes sense due to fuzzy types
  532. # pylint: disable=maybe-no-member
  533. ind = None
  534. try:
  535. ind = entry.index(key_or_item)
  536. except ValueError:
  537. return (False, self.yaml_dict)
  538. entry.pop(ind)
  539. return (True, self.yaml_dict)
  540. return (False, self.yaml_dict)
  541. def delete(self, path, index=None, value=None):
  542. ''' remove path from a dict'''
  543. try:
  544. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  545. except KeyError:
  546. entry = None
  547. if entry is None:
  548. return (False, self.yaml_dict)
  549. result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
  550. if not result:
  551. return (False, self.yaml_dict)
  552. return (True, self.yaml_dict)
  553. def exists(self, path, value):
  554. ''' check if value exists at path'''
  555. try:
  556. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  557. except KeyError:
  558. entry = None
  559. if isinstance(entry, list):
  560. if value in entry:
  561. return True
  562. return False
  563. elif isinstance(entry, dict):
  564. if isinstance(value, dict):
  565. rval = False
  566. for key, val in value.items():
  567. if entry[key] != val:
  568. rval = False
  569. break
  570. else:
  571. rval = True
  572. return rval
  573. return value in entry
  574. return entry == value
  575. def append(self, path, value):
  576. '''append value to a list'''
  577. try:
  578. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  579. except KeyError:
  580. entry = None
  581. if entry is None:
  582. self.put(path, [])
  583. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  584. if not isinstance(entry, list):
  585. return (False, self.yaml_dict)
  586. # AUDIT:maybe-no-member makes sense due to loading data from
  587. # a serialized format.
  588. # pylint: disable=maybe-no-member
  589. entry.append(value)
  590. return (True, self.yaml_dict)
  591. # pylint: disable=too-many-arguments
  592. def update(self, path, value, index=None, curr_value=None):
  593. ''' put path, value into a dict '''
  594. try:
  595. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  596. except KeyError:
  597. entry = None
  598. if isinstance(entry, dict):
  599. # AUDIT:maybe-no-member makes sense due to fuzzy types
  600. # pylint: disable=maybe-no-member
  601. if not isinstance(value, dict):
  602. raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
  603. 'value=[{}] type=[{}]'.format(value, type(value)))
  604. entry.update(value)
  605. return (True, self.yaml_dict)
  606. elif isinstance(entry, list):
  607. # AUDIT:maybe-no-member makes sense due to fuzzy types
  608. # pylint: disable=maybe-no-member
  609. ind = None
  610. if curr_value:
  611. try:
  612. ind = entry.index(curr_value)
  613. except ValueError:
  614. return (False, self.yaml_dict)
  615. elif index is not None:
  616. ind = index
  617. if ind is not None and entry[ind] != value:
  618. entry[ind] = value
  619. return (True, self.yaml_dict)
  620. # see if it exists in the list
  621. try:
  622. ind = entry.index(value)
  623. except ValueError:
  624. # doesn't exist, append it
  625. entry.append(value)
  626. return (True, self.yaml_dict)
  627. # already exists, return
  628. if ind is not None:
  629. return (False, self.yaml_dict)
  630. return (False, self.yaml_dict)
  631. def put(self, path, value):
  632. ''' put path, value into a dict '''
  633. try:
  634. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  635. except KeyError:
  636. entry = None
  637. if entry == value:
  638. return (False, self.yaml_dict)
  639. # deepcopy didn't work
  640. # Try to use ruamel.yaml and fallback to pyyaml
  641. try:
  642. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  643. default_flow_style=False),
  644. yaml.RoundTripLoader)
  645. except AttributeError:
  646. tmp_copy = copy.deepcopy(self.yaml_dict)
  647. # set the format attributes if available
  648. try:
  649. tmp_copy.fa.set_block_style()
  650. except AttributeError:
  651. pass
  652. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  653. if result is None:
  654. return (False, self.yaml_dict)
  655. # When path equals "" it is a special case.
  656. # "" refers to the root of the document
  657. # Only update the root path (entire document) when its a list or dict
  658. if path == '':
  659. if isinstance(result, list) or isinstance(result, dict):
  660. self.yaml_dict = result
  661. return (True, self.yaml_dict)
  662. return (False, self.yaml_dict)
  663. self.yaml_dict = tmp_copy
  664. return (True, self.yaml_dict)
  665. def create(self, path, value):
  666. ''' create a yaml file '''
  667. if not self.file_exists():
  668. # deepcopy didn't work
  669. # Try to use ruamel.yaml and fallback to pyyaml
  670. try:
  671. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  672. default_flow_style=False),
  673. yaml.RoundTripLoader)
  674. except AttributeError:
  675. tmp_copy = copy.deepcopy(self.yaml_dict)
  676. # set the format attributes if available
  677. try:
  678. tmp_copy.fa.set_block_style()
  679. except AttributeError:
  680. pass
  681. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  682. if result is not None:
  683. self.yaml_dict = tmp_copy
  684. return (True, self.yaml_dict)
  685. return (False, self.yaml_dict)
  686. @staticmethod
  687. def get_curr_value(invalue, val_type):
  688. '''return the current value'''
  689. if invalue is None:
  690. return None
  691. curr_value = invalue
  692. if val_type == 'yaml':
  693. curr_value = yaml.safe_load(str(invalue))
  694. elif val_type == 'json':
  695. curr_value = json.loads(invalue)
  696. return curr_value
  697. @staticmethod
  698. def parse_value(inc_value, vtype=''):
  699. '''determine value type passed'''
  700. true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
  701. 'on', 'On', 'ON', ]
  702. false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
  703. 'off', 'Off', 'OFF']
  704. # It came in as a string but you didn't specify value_type as string
  705. # we will convert to bool if it matches any of the above cases
  706. if isinstance(inc_value, str) and 'bool' in vtype:
  707. if inc_value not in true_bools and inc_value not in false_bools:
  708. raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
  709. elif isinstance(inc_value, bool) and 'str' in vtype:
  710. inc_value = str(inc_value)
  711. # There is a special case where '' will turn into None after yaml loading it so skip
  712. if isinstance(inc_value, str) and inc_value == '':
  713. pass
  714. # If vtype is not str then go ahead and attempt to yaml load it.
  715. elif isinstance(inc_value, str) and 'str' not in vtype:
  716. try:
  717. inc_value = yaml.safe_load(inc_value)
  718. except Exception:
  719. raise YeditException('Could not determine type of incoming value. ' +
  720. 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
  721. return inc_value
  722. @staticmethod
  723. def process_edits(edits, yamlfile):
  724. '''run through a list of edits and process them one-by-one'''
  725. results = []
  726. for edit in edits:
  727. value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
  728. if edit.get('action') == 'update':
  729. # pylint: disable=line-too-long
  730. curr_value = Yedit.get_curr_value(
  731. Yedit.parse_value(edit.get('curr_value')),
  732. edit.get('curr_value_format'))
  733. rval = yamlfile.update(edit['key'],
  734. value,
  735. edit.get('index'),
  736. curr_value)
  737. elif edit.get('action') == 'append':
  738. rval = yamlfile.append(edit['key'], value)
  739. else:
  740. rval = yamlfile.put(edit['key'], value)
  741. if rval[0]:
  742. results.append({'key': edit['key'], 'edit': rval[1]})
  743. return {'changed': len(results) > 0, 'results': results}
  744. # pylint: disable=too-many-return-statements,too-many-branches
  745. @staticmethod
  746. def run_ansible(params):
  747. '''perform the idempotent crud operations'''
  748. yamlfile = Yedit(filename=params['src'],
  749. backup=params['backup'],
  750. content_type=params['content_type'],
  751. backup_ext=params['backup_ext'],
  752. separator=params['separator'])
  753. state = params['state']
  754. if params['src']:
  755. rval = yamlfile.load()
  756. if yamlfile.yaml_dict is None and state != 'present':
  757. return {'failed': True,
  758. 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
  759. 'file exists, that it is has correct permissions, and is valid yaml.'}
  760. if state == 'list':
  761. if params['content']:
  762. content = Yedit.parse_value(params['content'], params['content_type'])
  763. yamlfile.yaml_dict = content
  764. if params['key']:
  765. rval = yamlfile.get(params['key'])
  766. return {'changed': False, 'result': rval, 'state': state}
  767. elif state == 'absent':
  768. if params['content']:
  769. content = Yedit.parse_value(params['content'], params['content_type'])
  770. yamlfile.yaml_dict = content
  771. if params['update']:
  772. rval = yamlfile.pop(params['key'], params['value'])
  773. else:
  774. rval = yamlfile.delete(params['key'], params['index'], params['value'])
  775. if rval[0] and params['src']:
  776. yamlfile.write()
  777. return {'changed': rval[0], 'result': rval[1], 'state': state}
  778. elif state == 'present':
  779. # check if content is different than what is in the file
  780. if params['content']:
  781. content = Yedit.parse_value(params['content'], params['content_type'])
  782. # We had no edits to make and the contents are the same
  783. if yamlfile.yaml_dict == content and \
  784. params['value'] is None:
  785. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  786. yamlfile.yaml_dict = content
  787. # If we were passed a key, value then
  788. # we enapsulate it in a list and process it
  789. # Key, Value passed to the module : Converted to Edits list #
  790. edits = []
  791. _edit = {}
  792. if params['value'] is not None:
  793. _edit['value'] = params['value']
  794. _edit['value_type'] = params['value_type']
  795. _edit['key'] = params['key']
  796. if params['update']:
  797. _edit['action'] = 'update'
  798. _edit['curr_value'] = params['curr_value']
  799. _edit['curr_value_format'] = params['curr_value_format']
  800. _edit['index'] = params['index']
  801. elif params['append']:
  802. _edit['action'] = 'append'
  803. edits.append(_edit)
  804. elif params['edits'] is not None:
  805. edits = params['edits']
  806. if edits:
  807. results = Yedit.process_edits(edits, yamlfile)
  808. # if there were changes and a src provided to us we need to write
  809. if results['changed'] and params['src']:
  810. yamlfile.write()
  811. return {'changed': results['changed'], 'result': results['results'], 'state': state}
  812. # no edits to make
  813. if params['src']:
  814. # pylint: disable=redefined-variable-type
  815. rval = yamlfile.write()
  816. return {'changed': rval[0],
  817. 'result': rval[1],
  818. 'state': state}
  819. # We were passed content but no src, key or value, or edits. Return contents in memory
  820. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  821. return {'failed': True, 'msg': 'Unkown state passed'}
  822. # -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
  823. # -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
  824. # pylint: disable=too-many-lines
  825. # noqa: E301,E302,E303,T001
  826. class OpenShiftCLIError(Exception):
  827. '''Exception class for openshiftcli'''
  828. pass
  829. ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
  830. def locate_oc_binary():
  831. ''' Find and return oc binary file '''
  832. # https://github.com/openshift/openshift-ansible/issues/3410
  833. # oc can be in /usr/local/bin in some cases, but that may not
  834. # be in $PATH due to ansible/sudo
  835. paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
  836. oc_binary = 'oc'
  837. # Use shutil.which if it is available, otherwise fallback to a naive path search
  838. try:
  839. which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
  840. if which_result is not None:
  841. oc_binary = which_result
  842. except AttributeError:
  843. for path in paths:
  844. if os.path.exists(os.path.join(path, oc_binary)):
  845. oc_binary = os.path.join(path, oc_binary)
  846. break
  847. return oc_binary
  848. # pylint: disable=too-few-public-methods
  849. class OpenShiftCLI(object):
  850. ''' Class to wrap the command line tools '''
  851. def __init__(self,
  852. namespace,
  853. kubeconfig='/etc/origin/master/admin.kubeconfig',
  854. verbose=False,
  855. all_namespaces=False):
  856. ''' Constructor for OpenshiftCLI '''
  857. self.namespace = namespace
  858. self.verbose = verbose
  859. self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
  860. self.all_namespaces = all_namespaces
  861. self.oc_binary = locate_oc_binary()
  862. # Pylint allows only 5 arguments to be passed.
  863. # pylint: disable=too-many-arguments
  864. def _replace_content(self, resource, rname, content, edits=None, force=False, sep='.'):
  865. ''' replace the current object with the content '''
  866. res = self._get(resource, rname)
  867. if not res['results']:
  868. return res
  869. fname = Utils.create_tmpfile(rname + '-')
  870. yed = Yedit(fname, res['results'][0], separator=sep)
  871. updated = False
  872. if content is not None:
  873. changes = []
  874. for key, value in content.items():
  875. changes.append(yed.put(key, value))
  876. if any([change[0] for change in changes]):
  877. updated = True
  878. elif edits is not None:
  879. results = Yedit.process_edits(edits, yed)
  880. if results['changed']:
  881. updated = True
  882. if updated:
  883. yed.write()
  884. atexit.register(Utils.cleanup, [fname])
  885. return self._replace(fname, force)
  886. return {'returncode': 0, 'updated': False}
  887. def _replace(self, fname, force=False):
  888. '''replace the current object with oc replace'''
  889. # We are removing the 'resourceVersion' to handle
  890. # a race condition when modifying oc objects
  891. yed = Yedit(fname)
  892. results = yed.delete('metadata.resourceVersion')
  893. if results[0]:
  894. yed.write()
  895. cmd = ['replace', '-f', fname]
  896. if force:
  897. cmd.append('--force')
  898. return self.openshift_cmd(cmd)
  899. def _create_from_content(self, rname, content):
  900. '''create a temporary file and then call oc create on it'''
  901. fname = Utils.create_tmpfile(rname + '-')
  902. yed = Yedit(fname, content=content)
  903. yed.write()
  904. atexit.register(Utils.cleanup, [fname])
  905. return self._create(fname)
  906. def _create(self, fname):
  907. '''call oc create on a filename'''
  908. return self.openshift_cmd(['create', '-f', fname])
  909. def _delete(self, resource, name=None, selector=None):
  910. '''call oc delete on a resource'''
  911. cmd = ['delete', resource]
  912. if selector is not None:
  913. cmd.append('--selector={}'.format(selector))
  914. elif name is not None:
  915. cmd.append(name)
  916. else:
  917. raise OpenShiftCLIError('Either name or selector is required when calling delete.')
  918. return self.openshift_cmd(cmd)
  919. def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
  920. '''process a template
  921. template_name: the name of the template to process
  922. create: whether to send to oc create after processing
  923. params: the parameters for the template
  924. template_data: the incoming template's data; instead of a file
  925. '''
  926. cmd = ['process']
  927. if template_data:
  928. cmd.extend(['-f', '-'])
  929. else:
  930. cmd.append(template_name)
  931. if params:
  932. param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
  933. cmd.append('-p')
  934. cmd.extend(param_str)
  935. results = self.openshift_cmd(cmd, output=True, input_data=template_data)
  936. if results['returncode'] != 0 or not create:
  937. return results
  938. fname = Utils.create_tmpfile(template_name + '-')
  939. yed = Yedit(fname, results['results'])
  940. yed.write()
  941. atexit.register(Utils.cleanup, [fname])
  942. return self.openshift_cmd(['create', '-f', fname])
  943. def _get(self, resource, name=None, selector=None, field_selector=None):
  944. '''return a resource by name '''
  945. cmd = ['get', resource]
  946. if selector is not None:
  947. cmd.append('--selector={}'.format(selector))
  948. if field_selector is not None:
  949. cmd.append('--field-selector={}'.format(field_selector))
  950. # Name cannot be used with selector or field_selector.
  951. if selector is None and field_selector is None and name is not None:
  952. cmd.append(name)
  953. cmd.extend(['-o', 'json'])
  954. rval = self.openshift_cmd(cmd, output=True)
  955. # Ensure results are retuned in an array
  956. if 'items' in rval:
  957. rval['results'] = rval['items']
  958. elif not isinstance(rval['results'], list):
  959. rval['results'] = [rval['results']]
  960. return rval
  961. def _schedulable(self, node=None, selector=None, schedulable=True):
  962. ''' perform oadm manage-node scheduable '''
  963. cmd = ['manage-node']
  964. if node:
  965. cmd.extend(node)
  966. else:
  967. cmd.append('--selector={}'.format(selector))
  968. cmd.append('--schedulable={}'.format(schedulable))
  969. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
  970. def _list_pods(self, node=None, selector=None, pod_selector=None):
  971. ''' perform oadm list pods
  972. node: the node in which to list pods
  973. selector: the label selector filter if provided
  974. pod_selector: the pod selector filter if provided
  975. '''
  976. cmd = ['manage-node']
  977. if node:
  978. cmd.extend(node)
  979. else:
  980. cmd.append('--selector={}'.format(selector))
  981. if pod_selector:
  982. cmd.append('--pod-selector={}'.format(pod_selector))
  983. cmd.extend(['--list-pods', '-o', 'json'])
  984. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  985. # pylint: disable=too-many-arguments
  986. def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
  987. ''' perform oadm manage-node evacuate '''
  988. cmd = ['manage-node']
  989. if node:
  990. cmd.extend(node)
  991. else:
  992. cmd.append('--selector={}'.format(selector))
  993. if dry_run:
  994. cmd.append('--dry-run')
  995. if pod_selector:
  996. cmd.append('--pod-selector={}'.format(pod_selector))
  997. if grace_period:
  998. cmd.append('--grace-period={}'.format(int(grace_period)))
  999. if force:
  1000. cmd.append('--force')
  1001. cmd.append('--evacuate')
  1002. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  1003. def _version(self):
  1004. ''' return the openshift version'''
  1005. return self.openshift_cmd(['version'], output=True, output_type='raw')
  1006. def _import_image(self, url=None, name=None, tag=None):
  1007. ''' perform image import '''
  1008. cmd = ['import-image']
  1009. image = '{0}'.format(name)
  1010. if tag:
  1011. image += ':{0}'.format(tag)
  1012. cmd.append(image)
  1013. if url:
  1014. cmd.append('--from={0}/{1}'.format(url, image))
  1015. cmd.append('-n{0}'.format(self.namespace))
  1016. cmd.append('--confirm')
  1017. return self.openshift_cmd(cmd)
  1018. def _run(self, cmds, input_data):
  1019. ''' Actually executes the command. This makes mocking easier. '''
  1020. curr_env = os.environ.copy()
  1021. curr_env.update({'KUBECONFIG': self.kubeconfig})
  1022. proc = subprocess.Popen(cmds,
  1023. stdin=subprocess.PIPE,
  1024. stdout=subprocess.PIPE,
  1025. stderr=subprocess.PIPE,
  1026. env=curr_env)
  1027. stdout, stderr = proc.communicate(input_data)
  1028. return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
  1029. # pylint: disable=too-many-arguments,too-many-branches
  1030. def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
  1031. '''Base command for oc '''
  1032. cmds = [self.oc_binary]
  1033. if oadm:
  1034. cmds.append('adm')
  1035. cmds.extend(cmd)
  1036. if self.all_namespaces:
  1037. cmds.extend(['--all-namespaces'])
  1038. elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
  1039. cmds.extend(['-n', self.namespace])
  1040. if self.verbose:
  1041. print(' '.join(cmds))
  1042. try:
  1043. returncode, stdout, stderr = self._run(cmds, input_data)
  1044. except OSError as ex:
  1045. returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
  1046. rval = {"returncode": returncode,
  1047. "cmd": ' '.join(cmds)}
  1048. if output_type == 'json':
  1049. rval['results'] = {}
  1050. if output and stdout:
  1051. try:
  1052. rval['results'] = json.loads(stdout)
  1053. except ValueError as verr:
  1054. if "No JSON object could be decoded" in verr.args:
  1055. rval['err'] = verr.args
  1056. elif output_type == 'raw':
  1057. rval['results'] = stdout if output else ''
  1058. if self.verbose:
  1059. print("STDOUT: {0}".format(stdout))
  1060. print("STDERR: {0}".format(stderr))
  1061. if 'err' in rval or returncode != 0:
  1062. rval.update({"stderr": stderr,
  1063. "stdout": stdout})
  1064. return rval
  1065. class Utils(object): # pragma: no cover
  1066. ''' utilities for openshiftcli modules '''
  1067. @staticmethod
  1068. def _write(filename, contents):
  1069. ''' Actually write the file contents to disk. This helps with mocking. '''
  1070. with open(filename, 'w') as sfd:
  1071. sfd.write(str(contents))
  1072. @staticmethod
  1073. def create_tmp_file_from_contents(rname, data, ftype='yaml'):
  1074. ''' create a file in tmp with name and contents'''
  1075. tmp = Utils.create_tmpfile(prefix=rname)
  1076. if ftype == 'yaml':
  1077. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  1078. # pylint: disable=no-member
  1079. if hasattr(yaml, 'RoundTripDumper'):
  1080. Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
  1081. else:
  1082. Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
  1083. elif ftype == 'json':
  1084. Utils._write(tmp, json.dumps(data))
  1085. else:
  1086. Utils._write(tmp, data)
  1087. # Register cleanup when module is done
  1088. atexit.register(Utils.cleanup, [tmp])
  1089. return tmp
  1090. @staticmethod
  1091. def create_tmpfile_copy(inc_file):
  1092. '''create a temporary copy of a file'''
  1093. tmpfile = Utils.create_tmpfile('lib_openshift-')
  1094. Utils._write(tmpfile, open(inc_file).read())
  1095. # Cleanup the tmpfile
  1096. atexit.register(Utils.cleanup, [tmpfile])
  1097. return tmpfile
  1098. @staticmethod
  1099. def create_tmpfile(prefix='tmp'):
  1100. ''' Generates and returns a temporary file name '''
  1101. with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
  1102. return tmp.name
  1103. @staticmethod
  1104. def create_tmp_files_from_contents(content, content_type=None):
  1105. '''Turn an array of dict: filename, content into a files array'''
  1106. if not isinstance(content, list):
  1107. content = [content]
  1108. files = []
  1109. for item in content:
  1110. path = Utils.create_tmp_file_from_contents(item['path'] + '-',
  1111. item['data'],
  1112. ftype=content_type)
  1113. files.append({'name': os.path.basename(item['path']),
  1114. 'path': path})
  1115. return files
  1116. @staticmethod
  1117. def cleanup(files):
  1118. '''Clean up on exit '''
  1119. for sfile in files:
  1120. if os.path.exists(sfile):
  1121. if os.path.isdir(sfile):
  1122. shutil.rmtree(sfile)
  1123. elif os.path.isfile(sfile):
  1124. os.remove(sfile)
  1125. @staticmethod
  1126. def exists(results, _name):
  1127. ''' Check to see if the results include the name '''
  1128. if not results:
  1129. return False
  1130. if Utils.find_result(results, _name):
  1131. return True
  1132. return False
  1133. @staticmethod
  1134. def find_result(results, _name):
  1135. ''' Find the specified result by name'''
  1136. rval = None
  1137. for result in results:
  1138. if 'metadata' in result and result['metadata']['name'] == _name:
  1139. rval = result
  1140. break
  1141. return rval
  1142. @staticmethod
  1143. def get_resource_file(sfile, sfile_type='yaml'):
  1144. ''' return the service file '''
  1145. contents = None
  1146. with open(sfile) as sfd:
  1147. contents = sfd.read()
  1148. if sfile_type == 'yaml':
  1149. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  1150. # pylint: disable=no-member
  1151. if hasattr(yaml, 'RoundTripLoader'):
  1152. contents = yaml.load(contents, yaml.RoundTripLoader)
  1153. else:
  1154. contents = yaml.safe_load(contents)
  1155. elif sfile_type == 'json':
  1156. contents = json.loads(contents)
  1157. return contents
  1158. @staticmethod
  1159. def filter_versions(stdout):
  1160. ''' filter the oc version output '''
  1161. version_dict = {}
  1162. version_search = ['oc', 'openshift', 'kubernetes']
  1163. for line in stdout.strip().split('\n'):
  1164. for term in version_search:
  1165. if not line:
  1166. continue
  1167. if line.startswith(term):
  1168. version_dict[term] = line.split()[-1]
  1169. # horrible hack to get openshift version in Openshift 3.2
  1170. # By default "oc version in 3.2 does not return an "openshift" version
  1171. if "openshift" not in version_dict:
  1172. version_dict["openshift"] = version_dict["oc"]
  1173. return version_dict
  1174. @staticmethod
  1175. def add_custom_versions(versions):
  1176. ''' create custom versions strings '''
  1177. versions_dict = {}
  1178. for tech, version in versions.items():
  1179. # clean up "-" from version
  1180. if "-" in version:
  1181. version = version.split("-")[0]
  1182. if version.startswith('v'):
  1183. version = version[1:] # Remove the 'v' prefix
  1184. versions_dict[tech + '_numeric'] = version.split('+')[0]
  1185. # "3.3.0.33" is what we have, we want "3.3"
  1186. versions_dict[tech + '_short'] = "{}.{}".format(*version.split('.'))
  1187. return versions_dict
  1188. @staticmethod
  1189. def openshift_installed():
  1190. ''' check if openshift is installed '''
  1191. import rpm
  1192. transaction_set = rpm.TransactionSet()
  1193. rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
  1194. return rpmquery.count() > 0
  1195. # Disabling too-many-branches. This is a yaml dictionary comparison function
  1196. # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
  1197. @staticmethod
  1198. def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
  1199. ''' Given a user defined definition, compare it with the results given back by our query. '''
  1200. if user_def is None:
  1201. user_def = {}
  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.redhat.io/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 -*- -*- -*-