oc_adm_router.py 109 KB

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