oc_adm_router.py 109 KB

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