oc_route.py 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932
  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/route -*- -*- -*-
  51. DOCUMENTATION = '''
  52. ---
  53. module: oc_route
  54. short_description: Create, modify, and idempotently manage openshift routes.
  55. description:
  56. - Manage openshift route objects programmatically.
  57. options:
  58. state:
  59. description:
  60. - State controls the action that will be taken with resource
  61. - present - create the route
  62. - absent - remove the route
  63. - list - returns the current representiation of the route
  64. required: true
  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. - Name of the object that is being queried.
  83. required: false
  84. default: None
  85. aliases: []
  86. namespace:
  87. description:
  88. - The namespace where the object lives.
  89. required: false
  90. default: str
  91. aliases: []
  92. labels:
  93. description:
  94. - The labels to apply on the route
  95. required: false
  96. default: None
  97. aliases: []
  98. tls_termination:
  99. description:
  100. - The options for termination. e.g. reencrypt
  101. required: false
  102. default: None
  103. aliases: []
  104. dest_cacert_path:
  105. description:
  106. - The path to the dest_cacert
  107. required: false
  108. default: None
  109. aliases: []
  110. cacert_path:
  111. description:
  112. - The path to the cacert
  113. required: false
  114. default: None
  115. aliases: []
  116. cert_path:
  117. description:
  118. - The path to the cert
  119. required: false
  120. default: None
  121. aliases: []
  122. key_path:
  123. description:
  124. - The path to the key
  125. required: false
  126. default: None
  127. aliases: []
  128. dest_cacert_content:
  129. description:
  130. - The dest_cacert content
  131. required: false
  132. default: None
  133. aliases: []
  134. cacert_content:
  135. description:
  136. - The cacert content
  137. required: false
  138. default: None
  139. aliases: []
  140. cert_content:
  141. description:
  142. - The cert content
  143. required: false
  144. default: None
  145. aliases: []
  146. service_name:
  147. description:
  148. - The name of the service that this route points to.
  149. required: false
  150. default: None
  151. aliases: []
  152. host:
  153. description:
  154. - The host that the route will use. e.g. myapp.x.y.z
  155. required: false
  156. default: None
  157. aliases: []
  158. port:
  159. description:
  160. - The Name of the service port or number of the container port the route will route traffic to
  161. required: false
  162. default: None
  163. aliases: []
  164. author:
  165. - "Kenny Woodson <kwoodson@redhat.com>"
  166. extends_documentation_fragment: []
  167. '''
  168. EXAMPLES = '''
  169. - name: Configure certificates for reencrypt route
  170. oc_route:
  171. name: myapproute
  172. namespace: awesomeapp
  173. cert_path: "/etc/origin/master/named_certificates/myapp_cert
  174. key_path: "/etc/origin/master/named_certificates/myapp_key
  175. cacert_path: "/etc/origin/master/named_certificates/myapp_cacert
  176. dest_cacert_content: "{{ dest_cacert_content }}"
  177. service_name: myapp_php
  178. host: myapp.awesomeapp.openshift.com
  179. tls_termination: reencrypt
  180. run_once: true
  181. '''
  182. # -*- -*- -*- End included fragment: doc/route -*- -*- -*-
  183. # -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
  184. class YeditException(Exception): # pragma: no cover
  185. ''' Exception class for Yedit '''
  186. pass
  187. # pylint: disable=too-many-public-methods,too-many-instance-attributes
  188. class Yedit(object): # pragma: no cover
  189. ''' Class to modify yaml files '''
  190. re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
  191. re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
  192. com_sep = set(['.', '#', '|', ':'])
  193. # pylint: disable=too-many-arguments
  194. def __init__(self,
  195. filename=None,
  196. content=None,
  197. content_type='yaml',
  198. separator='.',
  199. backup_ext=None,
  200. backup=False):
  201. self.content = content
  202. self._separator = separator
  203. self.filename = filename
  204. self.__yaml_dict = content
  205. self.content_type = content_type
  206. self.backup = backup
  207. if backup_ext is None:
  208. self.backup_ext = ".{}".format(time.strftime("%Y%m%dT%H%M%S"))
  209. else:
  210. self.backup_ext = backup_ext
  211. self.load(content_type=self.content_type)
  212. if self.__yaml_dict is None:
  213. self.__yaml_dict = {}
  214. @property
  215. def separator(self):
  216. ''' getter method for separator '''
  217. return self._separator
  218. @separator.setter
  219. def separator(self, inc_sep):
  220. ''' setter method for separator '''
  221. self._separator = inc_sep
  222. @property
  223. def yaml_dict(self):
  224. ''' getter method for yaml_dict '''
  225. return self.__yaml_dict
  226. @yaml_dict.setter
  227. def yaml_dict(self, value):
  228. ''' setter method for yaml_dict '''
  229. self.__yaml_dict = value
  230. @staticmethod
  231. def parse_key(key, sep='.'):
  232. '''parse the key allowing the appropriate separator'''
  233. common_separators = list(Yedit.com_sep - set([sep]))
  234. return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
  235. @staticmethod
  236. def valid_key(key, sep='.'):
  237. '''validate the incoming key'''
  238. common_separators = list(Yedit.com_sep - set([sep]))
  239. if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
  240. return False
  241. return True
  242. # pylint: disable=too-many-return-statements,too-many-branches
  243. @staticmethod
  244. def remove_entry(data, key, index=None, value=None, sep='.'):
  245. ''' remove data at location key '''
  246. if key == '' and isinstance(data, dict):
  247. if value is not None:
  248. data.pop(value)
  249. elif index is not None:
  250. raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
  251. else:
  252. data.clear()
  253. return True
  254. elif key == '' and isinstance(data, list):
  255. ind = None
  256. if value is not None:
  257. try:
  258. ind = data.index(value)
  259. except ValueError:
  260. return False
  261. elif index is not None:
  262. ind = index
  263. else:
  264. del data[:]
  265. if ind is not None:
  266. data.pop(ind)
  267. return True
  268. if not (key and Yedit.valid_key(key, sep)) and \
  269. isinstance(data, (list, dict)):
  270. return None
  271. key_indexes = Yedit.parse_key(key, sep)
  272. for arr_ind, dict_key in key_indexes[:-1]:
  273. if dict_key and isinstance(data, dict):
  274. data = data.get(dict_key)
  275. elif (arr_ind and isinstance(data, list) and
  276. int(arr_ind) <= len(data) - 1):
  277. data = data[int(arr_ind)]
  278. else:
  279. return None
  280. # process last index for remove
  281. # expected list entry
  282. if key_indexes[-1][0]:
  283. if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  284. del data[int(key_indexes[-1][0])]
  285. return True
  286. # expected dict entry
  287. elif key_indexes[-1][1]:
  288. if isinstance(data, dict):
  289. del data[key_indexes[-1][1]]
  290. return True
  291. @staticmethod
  292. def add_entry(data, key, item=None, sep='.'):
  293. ''' Get an item from a dictionary with key notation a.b.c
  294. d = {'a': {'b': 'c'}}}
  295. key = a#b
  296. return c
  297. '''
  298. if key == '':
  299. pass
  300. elif (not (key and Yedit.valid_key(key, sep)) and
  301. isinstance(data, (list, dict))):
  302. return None
  303. key_indexes = Yedit.parse_key(key, sep)
  304. for arr_ind, dict_key in key_indexes[:-1]:
  305. if dict_key:
  306. if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
  307. data = data[dict_key]
  308. continue
  309. elif data and not isinstance(data, dict):
  310. raise YeditException("Unexpected item type found while going through key " +
  311. "path: {} (at key: {})".format(key, dict_key))
  312. data[dict_key] = {}
  313. data = data[dict_key]
  314. elif (arr_ind and isinstance(data, list) and
  315. int(arr_ind) <= len(data) - 1):
  316. data = data[int(arr_ind)]
  317. else:
  318. raise YeditException("Unexpected item type found while going through key path: {}".format(key))
  319. if key == '':
  320. data = item
  321. # process last index for add
  322. # expected list entry
  323. elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  324. data[int(key_indexes[-1][0])] = item
  325. # expected dict entry
  326. elif key_indexes[-1][1] and isinstance(data, dict):
  327. data[key_indexes[-1][1]] = item
  328. # didn't add/update to an existing list, nor add/update key to a dict
  329. # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
  330. # non-existent array
  331. else:
  332. raise YeditException("Error adding to object at path: {}".format(key))
  333. return data
  334. @staticmethod
  335. def get_entry(data, key, sep='.'):
  336. ''' Get an item from a dictionary with key notation a.b.c
  337. d = {'a': {'b': 'c'}}}
  338. key = a.b
  339. return c
  340. '''
  341. if key == '':
  342. pass
  343. elif (not (key and Yedit.valid_key(key, sep)) and
  344. isinstance(data, (list, dict))):
  345. return None
  346. key_indexes = Yedit.parse_key(key, sep)
  347. for arr_ind, dict_key in key_indexes:
  348. if dict_key and isinstance(data, dict):
  349. data = data.get(dict_key)
  350. elif (arr_ind and isinstance(data, list) and
  351. int(arr_ind) <= len(data) - 1):
  352. data = data[int(arr_ind)]
  353. else:
  354. return None
  355. return data
  356. @staticmethod
  357. def _write(filename, contents):
  358. ''' Actually write the file contents to disk. This helps with mocking. '''
  359. tmp_filename = filename + '.yedit'
  360. with open(tmp_filename, 'w') as yfd:
  361. fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  362. yfd.write(contents)
  363. fcntl.flock(yfd, fcntl.LOCK_UN)
  364. os.rename(tmp_filename, filename)
  365. def write(self):
  366. ''' write to file '''
  367. if not self.filename:
  368. raise YeditException('Please specify a filename.')
  369. if self.backup and self.file_exists():
  370. shutil.copy(self.filename, '{}{}'.format(self.filename, self.backup_ext))
  371. # Try to set format attributes if supported
  372. try:
  373. self.yaml_dict.fa.set_block_style()
  374. except AttributeError:
  375. pass
  376. # Try to use RoundTripDumper if supported.
  377. if self.content_type == 'yaml':
  378. try:
  379. Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
  380. except AttributeError:
  381. Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
  382. elif self.content_type == 'json':
  383. Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
  384. else:
  385. raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
  386. 'Please specify a content_type of yaml or json.')
  387. return (True, self.yaml_dict)
  388. def read(self):
  389. ''' read from file '''
  390. # check if it exists
  391. if self.filename is None or not self.file_exists():
  392. return None
  393. contents = None
  394. with open(self.filename) as yfd:
  395. contents = yfd.read()
  396. return contents
  397. def file_exists(self):
  398. ''' return whether file exists '''
  399. if os.path.exists(self.filename):
  400. return True
  401. return False
  402. def load(self, content_type='yaml'):
  403. ''' return yaml file '''
  404. contents = self.read()
  405. if not contents and not self.content:
  406. return None
  407. if self.content:
  408. if isinstance(self.content, dict):
  409. self.yaml_dict = self.content
  410. return self.yaml_dict
  411. elif isinstance(self.content, str):
  412. contents = self.content
  413. # check if it is yaml
  414. try:
  415. if content_type == 'yaml' and contents:
  416. # Try to set format attributes if supported
  417. try:
  418. self.yaml_dict.fa.set_block_style()
  419. except AttributeError:
  420. pass
  421. # Try to use RoundTripLoader if supported.
  422. try:
  423. self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
  424. except AttributeError:
  425. self.yaml_dict = yaml.safe_load(contents)
  426. # Try to set format attributes if supported
  427. try:
  428. self.yaml_dict.fa.set_block_style()
  429. except AttributeError:
  430. pass
  431. elif content_type == 'json' and contents:
  432. self.yaml_dict = json.loads(contents)
  433. except yaml.YAMLError as err:
  434. # Error loading yaml or json
  435. raise YeditException('Problem with loading yaml file. {}'.format(err))
  436. return self.yaml_dict
  437. def get(self, key):
  438. ''' get a specified key'''
  439. try:
  440. entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
  441. except KeyError:
  442. entry = None
  443. return entry
  444. def pop(self, path, key_or_item):
  445. ''' remove a key, value pair from a dict or an item for a list'''
  446. try:
  447. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  448. except KeyError:
  449. entry = None
  450. if entry is None:
  451. return (False, self.yaml_dict)
  452. if isinstance(entry, dict):
  453. # AUDIT:maybe-no-member makes sense due to fuzzy types
  454. # pylint: disable=maybe-no-member
  455. if key_or_item in entry:
  456. entry.pop(key_or_item)
  457. return (True, self.yaml_dict)
  458. return (False, self.yaml_dict)
  459. elif isinstance(entry, list):
  460. # AUDIT:maybe-no-member makes sense due to fuzzy types
  461. # pylint: disable=maybe-no-member
  462. ind = None
  463. try:
  464. ind = entry.index(key_or_item)
  465. except ValueError:
  466. return (False, self.yaml_dict)
  467. entry.pop(ind)
  468. return (True, self.yaml_dict)
  469. return (False, self.yaml_dict)
  470. def delete(self, path, index=None, value=None):
  471. ''' remove path from a dict'''
  472. try:
  473. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  474. except KeyError:
  475. entry = None
  476. if entry is None:
  477. return (False, self.yaml_dict)
  478. result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
  479. if not result:
  480. return (False, self.yaml_dict)
  481. return (True, self.yaml_dict)
  482. def exists(self, path, value):
  483. ''' check if value exists at path'''
  484. try:
  485. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  486. except KeyError:
  487. entry = None
  488. if isinstance(entry, list):
  489. if value in entry:
  490. return True
  491. return False
  492. elif isinstance(entry, dict):
  493. if isinstance(value, dict):
  494. rval = False
  495. for key, val in value.items():
  496. if entry[key] != val:
  497. rval = False
  498. break
  499. else:
  500. rval = True
  501. return rval
  502. return value in entry
  503. return entry == value
  504. def append(self, path, value):
  505. '''append value to a list'''
  506. try:
  507. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  508. except KeyError:
  509. entry = None
  510. if entry is None:
  511. self.put(path, [])
  512. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  513. if not isinstance(entry, list):
  514. return (False, self.yaml_dict)
  515. # AUDIT:maybe-no-member makes sense due to loading data from
  516. # a serialized format.
  517. # pylint: disable=maybe-no-member
  518. entry.append(value)
  519. return (True, self.yaml_dict)
  520. # pylint: disable=too-many-arguments
  521. def update(self, path, value, index=None, curr_value=None):
  522. ''' put path, value into a dict '''
  523. try:
  524. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  525. except KeyError:
  526. entry = None
  527. if isinstance(entry, dict):
  528. # AUDIT:maybe-no-member makes sense due to fuzzy types
  529. # pylint: disable=maybe-no-member
  530. if not isinstance(value, dict):
  531. raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
  532. 'value=[{}] type=[{}]'.format(value, type(value)))
  533. entry.update(value)
  534. return (True, self.yaml_dict)
  535. elif isinstance(entry, list):
  536. # AUDIT:maybe-no-member makes sense due to fuzzy types
  537. # pylint: disable=maybe-no-member
  538. ind = None
  539. if curr_value:
  540. try:
  541. ind = entry.index(curr_value)
  542. except ValueError:
  543. return (False, self.yaml_dict)
  544. elif index is not None:
  545. ind = index
  546. if ind is not None and entry[ind] != value:
  547. entry[ind] = value
  548. return (True, self.yaml_dict)
  549. # see if it exists in the list
  550. try:
  551. ind = entry.index(value)
  552. except ValueError:
  553. # doesn't exist, append it
  554. entry.append(value)
  555. return (True, self.yaml_dict)
  556. # already exists, return
  557. if ind is not None:
  558. return (False, self.yaml_dict)
  559. return (False, self.yaml_dict)
  560. def put(self, path, value):
  561. ''' put path, value into a dict '''
  562. try:
  563. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  564. except KeyError:
  565. entry = None
  566. if entry == value:
  567. return (False, self.yaml_dict)
  568. # deepcopy didn't work
  569. # Try to use ruamel.yaml and fallback to pyyaml
  570. try:
  571. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  572. default_flow_style=False),
  573. yaml.RoundTripLoader)
  574. except AttributeError:
  575. tmp_copy = copy.deepcopy(self.yaml_dict)
  576. # set the format attributes if available
  577. try:
  578. tmp_copy.fa.set_block_style()
  579. except AttributeError:
  580. pass
  581. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  582. if result is None:
  583. return (False, self.yaml_dict)
  584. # When path equals "" it is a special case.
  585. # "" refers to the root of the document
  586. # Only update the root path (entire document) when its a list or dict
  587. if path == '':
  588. if isinstance(result, list) or isinstance(result, dict):
  589. self.yaml_dict = result
  590. return (True, self.yaml_dict)
  591. return (False, self.yaml_dict)
  592. self.yaml_dict = tmp_copy
  593. return (True, self.yaml_dict)
  594. def create(self, path, value):
  595. ''' create a yaml file '''
  596. if not self.file_exists():
  597. # deepcopy didn't work
  598. # Try to use ruamel.yaml and fallback to pyyaml
  599. try:
  600. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  601. default_flow_style=False),
  602. yaml.RoundTripLoader)
  603. except AttributeError:
  604. tmp_copy = copy.deepcopy(self.yaml_dict)
  605. # set the format attributes if available
  606. try:
  607. tmp_copy.fa.set_block_style()
  608. except AttributeError:
  609. pass
  610. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  611. if result is not None:
  612. self.yaml_dict = tmp_copy
  613. return (True, self.yaml_dict)
  614. return (False, self.yaml_dict)
  615. @staticmethod
  616. def get_curr_value(invalue, val_type):
  617. '''return the current value'''
  618. if invalue is None:
  619. return None
  620. curr_value = invalue
  621. if val_type == 'yaml':
  622. curr_value = yaml.safe_load(str(invalue))
  623. elif val_type == 'json':
  624. curr_value = json.loads(invalue)
  625. return curr_value
  626. @staticmethod
  627. def parse_value(inc_value, vtype=''):
  628. '''determine value type passed'''
  629. true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
  630. 'on', 'On', 'ON', ]
  631. false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
  632. 'off', 'Off', 'OFF']
  633. # It came in as a string but you didn't specify value_type as string
  634. # we will convert to bool if it matches any of the above cases
  635. if isinstance(inc_value, str) and 'bool' in vtype:
  636. if inc_value not in true_bools and inc_value not in false_bools:
  637. raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
  638. elif isinstance(inc_value, bool) and 'str' in vtype:
  639. inc_value = str(inc_value)
  640. # There is a special case where '' will turn into None after yaml loading it so skip
  641. if isinstance(inc_value, str) and inc_value == '':
  642. pass
  643. # If vtype is not str then go ahead and attempt to yaml load it.
  644. elif isinstance(inc_value, str) and 'str' not in vtype:
  645. try:
  646. inc_value = yaml.safe_load(inc_value)
  647. except Exception:
  648. raise YeditException('Could not determine type of incoming value. ' +
  649. 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
  650. return inc_value
  651. @staticmethod
  652. def process_edits(edits, yamlfile):
  653. '''run through a list of edits and process them one-by-one'''
  654. results = []
  655. for edit in edits:
  656. value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
  657. if edit.get('action') == 'update':
  658. # pylint: disable=line-too-long
  659. curr_value = Yedit.get_curr_value(
  660. Yedit.parse_value(edit.get('curr_value')),
  661. edit.get('curr_value_format'))
  662. rval = yamlfile.update(edit['key'],
  663. value,
  664. edit.get('index'),
  665. curr_value)
  666. elif edit.get('action') == 'append':
  667. rval = yamlfile.append(edit['key'], value)
  668. else:
  669. rval = yamlfile.put(edit['key'], value)
  670. if rval[0]:
  671. results.append({'key': edit['key'], 'edit': rval[1]})
  672. return {'changed': len(results) > 0, 'results': results}
  673. # pylint: disable=too-many-return-statements,too-many-branches
  674. @staticmethod
  675. def run_ansible(params):
  676. '''perform the idempotent crud operations'''
  677. yamlfile = Yedit(filename=params['src'],
  678. backup=params['backup'],
  679. content_type=params['content_type'],
  680. backup_ext=params['backup_ext'],
  681. separator=params['separator'])
  682. state = params['state']
  683. if params['src']:
  684. rval = yamlfile.load()
  685. if yamlfile.yaml_dict is None and state != 'present':
  686. return {'failed': True,
  687. 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
  688. 'file exists, that it is has correct permissions, and is valid yaml.'}
  689. if state == 'list':
  690. if params['content']:
  691. content = Yedit.parse_value(params['content'], params['content_type'])
  692. yamlfile.yaml_dict = content
  693. if params['key']:
  694. rval = yamlfile.get(params['key'])
  695. return {'changed': False, 'result': rval, 'state': state}
  696. elif state == 'absent':
  697. if params['content']:
  698. content = Yedit.parse_value(params['content'], params['content_type'])
  699. yamlfile.yaml_dict = content
  700. if params['update']:
  701. rval = yamlfile.pop(params['key'], params['value'])
  702. else:
  703. rval = yamlfile.delete(params['key'], params['index'], params['value'])
  704. if rval[0] and params['src']:
  705. yamlfile.write()
  706. return {'changed': rval[0], 'result': rval[1], 'state': state}
  707. elif state == 'present':
  708. # check if content is different than what is in the file
  709. if params['content']:
  710. content = Yedit.parse_value(params['content'], params['content_type'])
  711. # We had no edits to make and the contents are the same
  712. if yamlfile.yaml_dict == content and \
  713. params['value'] is None:
  714. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  715. yamlfile.yaml_dict = content
  716. # If we were passed a key, value then
  717. # we enapsulate it in a list and process it
  718. # Key, Value passed to the module : Converted to Edits list #
  719. edits = []
  720. _edit = {}
  721. if params['value'] is not None:
  722. _edit['value'] = params['value']
  723. _edit['value_type'] = params['value_type']
  724. _edit['key'] = params['key']
  725. if params['update']:
  726. _edit['action'] = 'update'
  727. _edit['curr_value'] = params['curr_value']
  728. _edit['curr_value_format'] = params['curr_value_format']
  729. _edit['index'] = params['index']
  730. elif params['append']:
  731. _edit['action'] = 'append'
  732. edits.append(_edit)
  733. elif params['edits'] is not None:
  734. edits = params['edits']
  735. if edits:
  736. results = Yedit.process_edits(edits, yamlfile)
  737. # if there were changes and a src provided to us we need to write
  738. if results['changed'] and params['src']:
  739. yamlfile.write()
  740. return {'changed': results['changed'], 'result': results['results'], 'state': state}
  741. # no edits to make
  742. if params['src']:
  743. # pylint: disable=redefined-variable-type
  744. rval = yamlfile.write()
  745. return {'changed': rval[0],
  746. 'result': rval[1],
  747. 'state': state}
  748. # We were passed content but no src, key or value, or edits. Return contents in memory
  749. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  750. return {'failed': True, 'msg': 'Unkown state passed'}
  751. # -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
  752. # -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
  753. # pylint: disable=too-many-lines
  754. # noqa: E301,E302,E303,T001
  755. class OpenShiftCLIError(Exception):
  756. '''Exception class for openshiftcli'''
  757. pass
  758. ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
  759. def locate_oc_binary():
  760. ''' Find and return oc binary file '''
  761. # https://github.com/openshift/openshift-ansible/issues/3410
  762. # oc can be in /usr/local/bin in some cases, but that may not
  763. # be in $PATH due to ansible/sudo
  764. paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
  765. oc_binary = 'oc'
  766. # Use shutil.which if it is available, otherwise fallback to a naive path search
  767. try:
  768. which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
  769. if which_result is not None:
  770. oc_binary = which_result
  771. except AttributeError:
  772. for path in paths:
  773. if os.path.exists(os.path.join(path, oc_binary)):
  774. oc_binary = os.path.join(path, oc_binary)
  775. break
  776. return oc_binary
  777. # pylint: disable=too-few-public-methods
  778. class OpenShiftCLI(object):
  779. ''' Class to wrap the command line tools '''
  780. def __init__(self,
  781. namespace,
  782. kubeconfig='/etc/origin/master/admin.kubeconfig',
  783. verbose=False,
  784. all_namespaces=False):
  785. ''' Constructor for OpenshiftCLI '''
  786. self.namespace = namespace
  787. self.verbose = verbose
  788. self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
  789. self.all_namespaces = all_namespaces
  790. self.oc_binary = locate_oc_binary()
  791. # Pylint allows only 5 arguments to be passed.
  792. # pylint: disable=too-many-arguments
  793. def _replace_content(self, resource, rname, content, edits=None, force=False, sep='.'):
  794. ''' replace the current object with the content '''
  795. res = self._get(resource, rname)
  796. if not res['results']:
  797. return res
  798. fname = Utils.create_tmpfile(rname + '-')
  799. yed = Yedit(fname, res['results'][0], separator=sep)
  800. updated = False
  801. if content is not None:
  802. changes = []
  803. for key, value in content.items():
  804. changes.append(yed.put(key, value))
  805. if any([change[0] for change in changes]):
  806. updated = True
  807. elif edits is not None:
  808. results = Yedit.process_edits(edits, yed)
  809. if results['changed']:
  810. updated = True
  811. if updated:
  812. yed.write()
  813. atexit.register(Utils.cleanup, [fname])
  814. return self._replace(fname, force)
  815. return {'returncode': 0, 'updated': False}
  816. def _replace(self, fname, force=False):
  817. '''replace the current object with oc replace'''
  818. # We are removing the 'resourceVersion' to handle
  819. # a race condition when modifying oc objects
  820. yed = Yedit(fname)
  821. results = yed.delete('metadata.resourceVersion')
  822. if results[0]:
  823. yed.write()
  824. cmd = ['replace', '-f', fname]
  825. if force:
  826. cmd.append('--force')
  827. return self.openshift_cmd(cmd)
  828. def _create_from_content(self, rname, content):
  829. '''create a temporary file and then call oc create on it'''
  830. fname = Utils.create_tmpfile(rname + '-')
  831. yed = Yedit(fname, content=content)
  832. yed.write()
  833. atexit.register(Utils.cleanup, [fname])
  834. return self._create(fname)
  835. def _create(self, fname):
  836. '''call oc create on a filename'''
  837. return self.openshift_cmd(['create', '-f', fname])
  838. def _delete(self, resource, name=None, selector=None):
  839. '''call oc delete on a resource'''
  840. cmd = ['delete', resource]
  841. if selector is not None:
  842. cmd.append('--selector={}'.format(selector))
  843. elif name is not None:
  844. cmd.append(name)
  845. else:
  846. raise OpenShiftCLIError('Either name or selector is required when calling delete.')
  847. return self.openshift_cmd(cmd)
  848. def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
  849. '''process a template
  850. template_name: the name of the template to process
  851. create: whether to send to oc create after processing
  852. params: the parameters for the template
  853. template_data: the incoming template's data; instead of a file
  854. '''
  855. cmd = ['process']
  856. if template_data:
  857. cmd.extend(['-f', '-'])
  858. else:
  859. cmd.append(template_name)
  860. if params:
  861. param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
  862. cmd.append('-p')
  863. cmd.extend(param_str)
  864. results = self.openshift_cmd(cmd, output=True, input_data=template_data)
  865. if results['returncode'] != 0 or not create:
  866. return results
  867. fname = Utils.create_tmpfile(template_name + '-')
  868. yed = Yedit(fname, results['results'])
  869. yed.write()
  870. atexit.register(Utils.cleanup, [fname])
  871. return self.openshift_cmd(['create', '-f', fname])
  872. def _get(self, resource, name=None, selector=None, field_selector=None):
  873. '''return a resource by name '''
  874. cmd = ['get', resource]
  875. if selector is not None:
  876. cmd.append('--selector={}'.format(selector))
  877. if field_selector is not None:
  878. cmd.append('--field-selector={}'.format(field_selector))
  879. # Name cannot be used with selector or field_selector.
  880. if selector is None and field_selector is None and name is not None:
  881. cmd.append(name)
  882. cmd.extend(['-o', 'json'])
  883. rval = self.openshift_cmd(cmd, output=True)
  884. # Ensure results are retuned in an array
  885. if 'items' in rval:
  886. rval['results'] = rval['items']
  887. elif not isinstance(rval['results'], list):
  888. rval['results'] = [rval['results']]
  889. return rval
  890. def _schedulable(self, node=None, selector=None, schedulable=True):
  891. ''' perform oadm manage-node scheduable '''
  892. cmd = ['manage-node']
  893. if node:
  894. cmd.extend(node)
  895. else:
  896. cmd.append('--selector={}'.format(selector))
  897. cmd.append('--schedulable={}'.format(schedulable))
  898. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
  899. def _list_pods(self, node=None, selector=None, pod_selector=None):
  900. ''' perform oadm list pods
  901. node: the node in which to list pods
  902. selector: the label selector filter if provided
  903. pod_selector: the pod selector filter if provided
  904. '''
  905. cmd = ['manage-node']
  906. if node:
  907. cmd.extend(node)
  908. else:
  909. cmd.append('--selector={}'.format(selector))
  910. if pod_selector:
  911. cmd.append('--pod-selector={}'.format(pod_selector))
  912. cmd.extend(['--list-pods', '-o', 'json'])
  913. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  914. # pylint: disable=too-many-arguments
  915. def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
  916. ''' perform oadm manage-node evacuate '''
  917. cmd = ['manage-node']
  918. if node:
  919. cmd.extend(node)
  920. else:
  921. cmd.append('--selector={}'.format(selector))
  922. if dry_run:
  923. cmd.append('--dry-run')
  924. if pod_selector:
  925. cmd.append('--pod-selector={}'.format(pod_selector))
  926. if grace_period:
  927. cmd.append('--grace-period={}'.format(int(grace_period)))
  928. if force:
  929. cmd.append('--force')
  930. cmd.append('--evacuate')
  931. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  932. def _version(self):
  933. ''' return the openshift version'''
  934. return self.openshift_cmd(['version'], output=True, output_type='raw')
  935. def _import_image(self, url=None, name=None, tag=None):
  936. ''' perform image import '''
  937. cmd = ['import-image']
  938. image = '{0}'.format(name)
  939. if tag:
  940. image += ':{0}'.format(tag)
  941. cmd.append(image)
  942. if url:
  943. cmd.append('--from={0}/{1}'.format(url, image))
  944. cmd.append('-n{0}'.format(self.namespace))
  945. cmd.append('--confirm')
  946. return self.openshift_cmd(cmd)
  947. def _run(self, cmds, input_data):
  948. ''' Actually executes the command. This makes mocking easier. '''
  949. curr_env = os.environ.copy()
  950. curr_env.update({'KUBECONFIG': self.kubeconfig})
  951. proc = subprocess.Popen(cmds,
  952. stdin=subprocess.PIPE,
  953. stdout=subprocess.PIPE,
  954. stderr=subprocess.PIPE,
  955. env=curr_env)
  956. stdout, stderr = proc.communicate(input_data)
  957. return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
  958. # pylint: disable=too-many-arguments,too-many-branches
  959. def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
  960. '''Base command for oc '''
  961. cmds = [self.oc_binary]
  962. if oadm:
  963. cmds.append('adm')
  964. cmds.extend(cmd)
  965. if self.all_namespaces:
  966. cmds.extend(['--all-namespaces'])
  967. elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
  968. cmds.extend(['-n', self.namespace])
  969. if self.verbose:
  970. print(' '.join(cmds))
  971. try:
  972. returncode, stdout, stderr = self._run(cmds, input_data)
  973. except OSError as ex:
  974. returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
  975. rval = {"returncode": returncode,
  976. "cmd": ' '.join(cmds)}
  977. if output_type == 'json':
  978. rval['results'] = {}
  979. if output and stdout:
  980. try:
  981. rval['results'] = json.loads(stdout)
  982. except ValueError as verr:
  983. if "No JSON object could be decoded" in verr.args:
  984. rval['err'] = verr.args
  985. elif output_type == 'raw':
  986. rval['results'] = stdout if output else ''
  987. if self.verbose:
  988. print("STDOUT: {0}".format(stdout))
  989. print("STDERR: {0}".format(stderr))
  990. if 'err' in rval or returncode != 0:
  991. rval.update({"stderr": stderr,
  992. "stdout": stdout})
  993. return rval
  994. class Utils(object): # pragma: no cover
  995. ''' utilities for openshiftcli modules '''
  996. @staticmethod
  997. def _write(filename, contents):
  998. ''' Actually write the file contents to disk. This helps with mocking. '''
  999. with open(filename, 'w') as sfd:
  1000. sfd.write(str(contents))
  1001. @staticmethod
  1002. def create_tmp_file_from_contents(rname, data, ftype='yaml'):
  1003. ''' create a file in tmp with name and contents'''
  1004. tmp = Utils.create_tmpfile(prefix=rname)
  1005. if ftype == 'yaml':
  1006. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  1007. # pylint: disable=no-member
  1008. if hasattr(yaml, 'RoundTripDumper'):
  1009. Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
  1010. else:
  1011. Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
  1012. elif ftype == 'json':
  1013. Utils._write(tmp, json.dumps(data))
  1014. else:
  1015. Utils._write(tmp, data)
  1016. # Register cleanup when module is done
  1017. atexit.register(Utils.cleanup, [tmp])
  1018. return tmp
  1019. @staticmethod
  1020. def create_tmpfile_copy(inc_file):
  1021. '''create a temporary copy of a file'''
  1022. tmpfile = Utils.create_tmpfile('lib_openshift-')
  1023. Utils._write(tmpfile, open(inc_file).read())
  1024. # Cleanup the tmpfile
  1025. atexit.register(Utils.cleanup, [tmpfile])
  1026. return tmpfile
  1027. @staticmethod
  1028. def create_tmpfile(prefix='tmp'):
  1029. ''' Generates and returns a temporary file name '''
  1030. with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
  1031. return tmp.name
  1032. @staticmethod
  1033. def create_tmp_files_from_contents(content, content_type=None):
  1034. '''Turn an array of dict: filename, content into a files array'''
  1035. if not isinstance(content, list):
  1036. content = [content]
  1037. files = []
  1038. for item in content:
  1039. path = Utils.create_tmp_file_from_contents(item['path'] + '-',
  1040. item['data'],
  1041. ftype=content_type)
  1042. files.append({'name': os.path.basename(item['path']),
  1043. 'path': path})
  1044. return files
  1045. @staticmethod
  1046. def cleanup(files):
  1047. '''Clean up on exit '''
  1048. for sfile in files:
  1049. if os.path.exists(sfile):
  1050. if os.path.isdir(sfile):
  1051. shutil.rmtree(sfile)
  1052. elif os.path.isfile(sfile):
  1053. os.remove(sfile)
  1054. @staticmethod
  1055. def exists(results, _name):
  1056. ''' Check to see if the results include the name '''
  1057. if not results:
  1058. return False
  1059. if Utils.find_result(results, _name):
  1060. return True
  1061. return False
  1062. @staticmethod
  1063. def find_result(results, _name):
  1064. ''' Find the specified result by name'''
  1065. rval = None
  1066. for result in results:
  1067. if 'metadata' in result and result['metadata']['name'] == _name:
  1068. rval = result
  1069. break
  1070. return rval
  1071. @staticmethod
  1072. def get_resource_file(sfile, sfile_type='yaml'):
  1073. ''' return the service file '''
  1074. contents = None
  1075. with open(sfile) as sfd:
  1076. contents = sfd.read()
  1077. if sfile_type == 'yaml':
  1078. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  1079. # pylint: disable=no-member
  1080. if hasattr(yaml, 'RoundTripLoader'):
  1081. contents = yaml.load(contents, yaml.RoundTripLoader)
  1082. else:
  1083. contents = yaml.safe_load(contents)
  1084. elif sfile_type == 'json':
  1085. contents = json.loads(contents)
  1086. return contents
  1087. @staticmethod
  1088. def filter_versions(stdout):
  1089. ''' filter the oc version output '''
  1090. version_dict = {}
  1091. version_search = ['oc', 'openshift', 'kubernetes']
  1092. for line in stdout.strip().split('\n'):
  1093. for term in version_search:
  1094. if not line:
  1095. continue
  1096. if line.startswith(term):
  1097. version_dict[term] = line.split()[-1]
  1098. # horrible hack to get openshift version in Openshift 3.2
  1099. # By default "oc version in 3.2 does not return an "openshift" version
  1100. if "openshift" not in version_dict:
  1101. version_dict["openshift"] = version_dict["oc"]
  1102. return version_dict
  1103. @staticmethod
  1104. def add_custom_versions(versions):
  1105. ''' create custom versions strings '''
  1106. versions_dict = {}
  1107. for tech, version in versions.items():
  1108. # clean up "-" from version
  1109. if "-" in version:
  1110. version = version.split("-")[0]
  1111. if version.startswith('v'):
  1112. version = version[1:] # Remove the 'v' prefix
  1113. versions_dict[tech + '_numeric'] = version.split('+')[0]
  1114. # "3.3.0.33" is what we have, we want "3.3"
  1115. versions_dict[tech + '_short'] = "{}.{}".format(*version.split('.'))
  1116. return versions_dict
  1117. @staticmethod
  1118. def openshift_installed():
  1119. ''' check if openshift is installed '''
  1120. import rpm
  1121. transaction_set = rpm.TransactionSet()
  1122. rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
  1123. return rpmquery.count() > 0
  1124. # Disabling too-many-branches. This is a yaml dictionary comparison function
  1125. # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
  1126. @staticmethod
  1127. def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
  1128. ''' Given a user defined definition, compare it with the results given back by our query. '''
  1129. # Currently these values are autogenerated and we do not need to check them
  1130. skip = ['metadata', 'status']
  1131. if skip_keys:
  1132. skip.extend(skip_keys)
  1133. for key, value in result_def.items():
  1134. if key in skip:
  1135. continue
  1136. # Both are lists
  1137. if isinstance(value, list):
  1138. if key not in user_def:
  1139. if debug:
  1140. print('User data does not have key [%s]' % key)
  1141. print('User data: %s' % user_def)
  1142. return False
  1143. if not isinstance(user_def[key], list):
  1144. if debug:
  1145. print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
  1146. return False
  1147. if len(user_def[key]) != len(value):
  1148. if debug:
  1149. print("List lengths are not equal.")
  1150. print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
  1151. print("user_def: %s" % user_def[key])
  1152. print("value: %s" % value)
  1153. return False
  1154. for values in zip(user_def[key], value):
  1155. if isinstance(values[0], dict) and isinstance(values[1], dict):
  1156. if debug:
  1157. print('sending list - list')
  1158. print(type(values[0]))
  1159. print(type(values[1]))
  1160. result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
  1161. if not result:
  1162. print('list compare returned false')
  1163. return False
  1164. elif value != user_def[key]:
  1165. if debug:
  1166. print('value should be identical')
  1167. print(user_def[key])
  1168. print(value)
  1169. return False
  1170. # recurse on a dictionary
  1171. elif isinstance(value, dict):
  1172. if key not in user_def:
  1173. if debug:
  1174. print("user_def does not have key [%s]" % key)
  1175. return False
  1176. if not isinstance(user_def[key], dict):
  1177. if debug:
  1178. print("dict returned false: not instance of dict")
  1179. return False
  1180. # before passing ensure keys match
  1181. api_values = set(value.keys()) - set(skip)
  1182. user_values = set(user_def[key].keys()) - set(skip)
  1183. if api_values != user_values:
  1184. if debug:
  1185. print("keys are not equal in dict")
  1186. print(user_values)
  1187. print(api_values)
  1188. return False
  1189. result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
  1190. if not result:
  1191. if debug:
  1192. print("dict returned false")
  1193. print(result)
  1194. return False
  1195. # Verify each key, value pair is the same
  1196. else:
  1197. if key not in user_def or value != user_def[key]:
  1198. if debug:
  1199. print("value not equal; user_def does not have key")
  1200. print(key)
  1201. print(value)
  1202. if key in user_def:
  1203. print(user_def[key])
  1204. return False
  1205. if debug:
  1206. print('returning true')
  1207. return True
  1208. class OpenShiftCLIConfig(object):
  1209. '''Generic Config'''
  1210. def __init__(self, rname, namespace, kubeconfig, options):
  1211. self.kubeconfig = kubeconfig
  1212. self.name = rname
  1213. self.namespace = namespace
  1214. self._options = options
  1215. @property
  1216. def config_options(self):
  1217. ''' return config options '''
  1218. return self._options
  1219. def to_option_list(self, ascommalist=''):
  1220. '''return all options as a string
  1221. if ascommalist is set to the name of a key, and
  1222. the value of that key is a dict, format the dict
  1223. as a list of comma delimited key=value pairs'''
  1224. return self.stringify(ascommalist)
  1225. def stringify(self, ascommalist=''):
  1226. ''' return the options hash as cli params in a string
  1227. if ascommalist is set to the name of a key, and
  1228. the value of that key is a dict, format the dict
  1229. as a list of comma delimited key=value pairs '''
  1230. rval = []
  1231. for key in sorted(self.config_options.keys()):
  1232. data = self.config_options[key]
  1233. if data['include'] \
  1234. and (data['value'] is not None or isinstance(data['value'], int)):
  1235. if key == ascommalist:
  1236. val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
  1237. else:
  1238. val = data['value']
  1239. rval.append('--{}={}'.format(key.replace('_', '-'), val))
  1240. return rval
  1241. # -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
  1242. # -*- -*- -*- Begin included fragment: lib/route.py -*- -*- -*-
  1243. # noqa: E302,E301
  1244. # pylint: disable=too-many-instance-attributes
  1245. class RouteConfig(object):
  1246. ''' Handle route options '''
  1247. # pylint: disable=too-many-arguments
  1248. def __init__(self,
  1249. sname,
  1250. namespace,
  1251. kubeconfig,
  1252. labels=None,
  1253. destcacert=None,
  1254. cacert=None,
  1255. cert=None,
  1256. key=None,
  1257. host=None,
  1258. tls_termination=None,
  1259. service_name=None,
  1260. wildcard_policy=None,
  1261. weight=None,
  1262. port=None):
  1263. ''' constructor for handling route options '''
  1264. self.kubeconfig = kubeconfig
  1265. self.name = sname
  1266. self.namespace = namespace
  1267. self.labels = labels
  1268. self.host = host
  1269. self.tls_termination = tls_termination
  1270. self.destcacert = destcacert
  1271. self.cacert = cacert
  1272. self.cert = cert
  1273. self.key = key
  1274. self.service_name = service_name
  1275. self.port = port
  1276. self.data = {}
  1277. self.wildcard_policy = wildcard_policy
  1278. if wildcard_policy is None:
  1279. self.wildcard_policy = 'None'
  1280. self.weight = weight
  1281. if weight is None:
  1282. self.weight = 100
  1283. self.create_dict()
  1284. def create_dict(self):
  1285. ''' return a service as a dict '''
  1286. self.data['apiVersion'] = 'v1'
  1287. self.data['kind'] = 'Route'
  1288. self.data['metadata'] = {}
  1289. self.data['metadata']['name'] = self.name
  1290. self.data['metadata']['namespace'] = self.namespace
  1291. if self.labels:
  1292. self.data['metadata']['labels'] = self.labels
  1293. self.data['spec'] = {}
  1294. self.data['spec']['host'] = self.host
  1295. if self.tls_termination:
  1296. self.data['spec']['tls'] = {}
  1297. self.data['spec']['tls']['termination'] = self.tls_termination
  1298. if self.tls_termination != 'passthrough':
  1299. self.data['spec']['tls']['key'] = self.key
  1300. self.data['spec']['tls']['caCertificate'] = self.cacert
  1301. self.data['spec']['tls']['certificate'] = self.cert
  1302. if self.tls_termination == 'reencrypt':
  1303. self.data['spec']['tls']['destinationCACertificate'] = self.destcacert
  1304. self.data['spec']['to'] = {'kind': 'Service',
  1305. 'name': self.service_name,
  1306. 'weight': self.weight}
  1307. self.data['spec']['wildcardPolicy'] = self.wildcard_policy
  1308. if self.port:
  1309. self.data['spec']['port'] = {}
  1310. self.data['spec']['port']['targetPort'] = self.port
  1311. # pylint: disable=too-many-instance-attributes,too-many-public-methods
  1312. class Route(Yedit):
  1313. ''' Class to wrap the oc command line tools '''
  1314. wildcard_policy = "spec.wildcardPolicy"
  1315. host_path = "spec.host"
  1316. port_path = "spec.port.targetPort"
  1317. service_path = "spec.to.name"
  1318. weight_path = "spec.to.weight"
  1319. cert_path = "spec.tls.certificate"
  1320. cacert_path = "spec.tls.caCertificate"
  1321. destcacert_path = "spec.tls.destinationCACertificate"
  1322. termination_path = "spec.tls.termination"
  1323. key_path = "spec.tls.key"
  1324. kind = 'route'
  1325. def __init__(self, content):
  1326. '''Route constructor'''
  1327. super(Route, self).__init__(content=content)
  1328. def get_destcacert(self):
  1329. ''' return cert '''
  1330. return self.get(Route.destcacert_path)
  1331. def get_cert(self):
  1332. ''' return cert '''
  1333. return self.get(Route.cert_path)
  1334. def get_key(self):
  1335. ''' return key '''
  1336. return self.get(Route.key_path)
  1337. def get_cacert(self):
  1338. ''' return cacert '''
  1339. return self.get(Route.cacert_path)
  1340. def get_service(self):
  1341. ''' return service name '''
  1342. return self.get(Route.service_path)
  1343. def get_weight(self):
  1344. ''' return service weight '''
  1345. return self.get(Route.weight_path)
  1346. def get_termination(self):
  1347. ''' return tls termination'''
  1348. return self.get(Route.termination_path)
  1349. def get_host(self):
  1350. ''' return host '''
  1351. return self.get(Route.host_path)
  1352. def get_port(self):
  1353. ''' return port '''
  1354. return self.get(Route.port_path)
  1355. def get_wildcard_policy(self):
  1356. ''' return wildcardPolicy '''
  1357. return self.get(Route.wildcard_policy)
  1358. # -*- -*- -*- End included fragment: lib/route.py -*- -*- -*-
  1359. # -*- -*- -*- Begin included fragment: class/oc_route.py -*- -*- -*-
  1360. # pylint: disable=too-many-instance-attributes
  1361. class OCRoute(OpenShiftCLI):
  1362. ''' Class to wrap the oc command line tools '''
  1363. kind = 'route'
  1364. def __init__(self,
  1365. config,
  1366. verbose=False):
  1367. ''' Constructor for OCVolume '''
  1368. super(OCRoute, self).__init__(config.namespace, kubeconfig=config.kubeconfig, verbose=verbose)
  1369. self.config = config
  1370. self._route = None
  1371. @property
  1372. def route(self):
  1373. ''' property function for route'''
  1374. if not self._route:
  1375. self.get()
  1376. return self._route
  1377. @route.setter
  1378. def route(self, data):
  1379. ''' setter function for route '''
  1380. self._route = data
  1381. def exists(self):
  1382. ''' return whether a route exists '''
  1383. if self.route:
  1384. return True
  1385. return False
  1386. def get(self):
  1387. '''return route information '''
  1388. result = self._get(self.kind, self.config.name)
  1389. if result['returncode'] == 0:
  1390. self.route = Route(content=result['results'][0])
  1391. elif 'routes \"%s\" not found' % self.config.name in result['stderr']:
  1392. result['returncode'] = 0
  1393. result['results'] = [{}]
  1394. elif 'namespaces \"%s\" not found' % self.config.namespace in result['stderr']:
  1395. result['returncode'] = 0
  1396. result['results'] = [{}]
  1397. return result
  1398. def delete(self):
  1399. '''delete the object'''
  1400. return self._delete(self.kind, self.config.name)
  1401. def create(self):
  1402. '''create the object'''
  1403. return self._create_from_content(self.config.name, self.config.data)
  1404. def update(self):
  1405. '''update the object'''
  1406. return self._replace_content(self.kind,
  1407. self.config.name,
  1408. self.config.data,
  1409. force=(self.config.host != self.route.get_host()))
  1410. def needs_update(self):
  1411. ''' verify an update is needed '''
  1412. skip = []
  1413. return not Utils.check_def_equal(self.config.data, self.route.yaml_dict, skip_keys=skip, debug=self.verbose)
  1414. @staticmethod
  1415. def get_cert_data(path, content):
  1416. '''get the data for a particular value'''
  1417. rval = None
  1418. if path and os.path.exists(path) and os.access(path, os.R_OK):
  1419. rval = open(path).read()
  1420. elif content:
  1421. rval = content
  1422. return rval
  1423. # pylint: disable=too-many-return-statements,too-many-branches
  1424. @staticmethod
  1425. def run_ansible(params, check_mode=False):
  1426. ''' run the oc_route module
  1427. params comes from the ansible portion for this module
  1428. files: a dictionary for the certificates
  1429. {'cert': {'path': '',
  1430. 'content': '',
  1431. 'value': ''
  1432. }
  1433. }
  1434. check_mode: does the module support check mode. (module.check_mode)
  1435. '''
  1436. files = {'destcacert': {'path': params['dest_cacert_path'],
  1437. 'content': params['dest_cacert_content'],
  1438. 'value': None, },
  1439. 'cacert': {'path': params['cacert_path'],
  1440. 'content': params['cacert_content'],
  1441. 'value': None, },
  1442. 'cert': {'path': params['cert_path'],
  1443. 'content': params['cert_content'],
  1444. 'value': None, },
  1445. 'key': {'path': params['key_path'],
  1446. 'content': params['key_content'],
  1447. 'value': None, }, }
  1448. if params['tls_termination'] and params['tls_termination'].lower() != 'passthrough': # E501
  1449. for key, option in files.items():
  1450. if not option['path'] and not option['content']:
  1451. continue
  1452. option['value'] = OCRoute.get_cert_data(option['path'], option['content']) # E501
  1453. if not option['value']:
  1454. return {'failed': True,
  1455. 'msg': 'Verify that you pass a correct value for %s' % key}
  1456. rconfig = RouteConfig(params['name'],
  1457. params['namespace'],
  1458. params['kubeconfig'],
  1459. params['labels'],
  1460. files['destcacert']['value'],
  1461. files['cacert']['value'],
  1462. files['cert']['value'],
  1463. files['key']['value'],
  1464. params['host'],
  1465. params['tls_termination'],
  1466. params['service_name'],
  1467. params['wildcard_policy'],
  1468. params['weight'],
  1469. params['port'])
  1470. oc_route = OCRoute(rconfig, verbose=params['debug'])
  1471. state = params['state']
  1472. api_rval = oc_route.get()
  1473. #####
  1474. # Get
  1475. #####
  1476. if state == 'list':
  1477. return {'changed': False,
  1478. 'results': api_rval['results'],
  1479. 'state': 'list'}
  1480. ########
  1481. # Delete
  1482. ########
  1483. if state == 'absent':
  1484. if oc_route.exists():
  1485. if check_mode:
  1486. return {'changed': False, 'msg': 'CHECK_MODE: Would have performed a delete.'} # noqa: E501
  1487. api_rval = oc_route.delete()
  1488. return {'changed': True, 'results': api_rval, 'state': "absent"} # noqa: E501
  1489. return {'changed': False, 'state': 'absent'}
  1490. if state == 'present':
  1491. ########
  1492. # Create
  1493. ########
  1494. if not oc_route.exists():
  1495. if check_mode:
  1496. return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'} # noqa: E501
  1497. # Create it here
  1498. api_rval = oc_route.create()
  1499. if api_rval['returncode'] != 0:
  1500. return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
  1501. # return the created object
  1502. api_rval = oc_route.get()
  1503. if api_rval['returncode'] != 0:
  1504. return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
  1505. return {'changed': True, 'results': api_rval, 'state': "present"} # noqa: E501
  1506. ########
  1507. # Update
  1508. ########
  1509. if oc_route.needs_update():
  1510. if check_mode:
  1511. return {'changed': True, 'msg': 'CHECK_MODE: Would have performed an update.'} # noqa: E501
  1512. api_rval = oc_route.update()
  1513. if api_rval['returncode'] != 0:
  1514. return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
  1515. # return the created object
  1516. api_rval = oc_route.get()
  1517. if api_rval['returncode'] != 0:
  1518. return {'failed': True, 'msg': api_rval, 'state': "present"} # noqa: E501
  1519. return {'changed': True, 'results': api_rval, 'state': "present"} # noqa: E501
  1520. return {'changed': False, 'results': api_rval, 'state': "present"}
  1521. # catch all
  1522. return {'failed': True, 'msg': "Unknown State passed"}
  1523. # -*- -*- -*- End included fragment: class/oc_route.py -*- -*- -*-
  1524. # -*- -*- -*- Begin included fragment: ansible/oc_route.py -*- -*- -*-
  1525. # pylint: disable=too-many-branches
  1526. def main():
  1527. '''
  1528. ansible oc module for route
  1529. '''
  1530. module = AnsibleModule(
  1531. argument_spec=dict(
  1532. kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
  1533. state=dict(default='present', type='str',
  1534. choices=['present', 'absent', 'list']),
  1535. debug=dict(default=False, type='bool'),
  1536. labels=dict(default=None, type='dict'),
  1537. name=dict(default=None, required=True, type='str'),
  1538. namespace=dict(default=None, required=True, type='str'),
  1539. tls_termination=dict(default=None, type='str'),
  1540. dest_cacert_path=dict(default=None, type='str'),
  1541. cacert_path=dict(default=None, type='str'),
  1542. cert_path=dict(default=None, type='str'),
  1543. key_path=dict(default=None, type='str'),
  1544. dest_cacert_content=dict(default=None, type='str'),
  1545. cacert_content=dict(default=None, type='str'),
  1546. cert_content=dict(default=None, type='str'),
  1547. key_content=dict(default=None, type='str'),
  1548. service_name=dict(default=None, type='str'),
  1549. host=dict(default=None, type='str'),
  1550. wildcard_policy=dict(default=None, type='str'),
  1551. weight=dict(default=None, type='int'),
  1552. port=dict(default=None, type='int'),
  1553. ),
  1554. mutually_exclusive=[('dest_cacert_path', 'dest_cacert_content'),
  1555. ('cacert_path', 'cacert_content'),
  1556. ('cert_path', 'cert_content'),
  1557. ('key_path', 'key_content'), ],
  1558. supports_check_mode=True,
  1559. )
  1560. results = OCRoute.run_ansible(module.params, module.check_mode)
  1561. if 'failed' in results:
  1562. module.fail_json(**results)
  1563. module.exit_json(**results)
  1564. if __name__ == '__main__':
  1565. main()
  1566. # -*- -*- -*- End included fragment: ansible/oc_route.py -*- -*- -*-