oc_adm_router.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. # pylint: skip-file
  2. # flake8: noqa
  3. class RouterException(Exception):
  4. ''' Router exception'''
  5. pass
  6. class RouterConfig(OpenShiftCLIConfig):
  7. ''' RouterConfig is a DTO for the router. '''
  8. def __init__(self, rname, namespace, kubeconfig, router_options):
  9. super(RouterConfig, self).__init__(rname, namespace, kubeconfig, router_options)
  10. class Router(OpenShiftCLI):
  11. ''' Class to wrap the oc command line tools '''
  12. def __init__(self,
  13. router_config,
  14. verbose=False):
  15. ''' Constructor for OpenshiftOC
  16. a router consists of 3 or more parts
  17. - dc/router
  18. - svc/router
  19. - sa/router
  20. - secret/router-certs
  21. - clusterrolebinding/router-router-role
  22. '''
  23. super(Router, self).__init__(router_config.namespace, router_config.kubeconfig, verbose)
  24. self.config = router_config
  25. self.verbose = verbose
  26. self.router_parts = [{'kind': 'dc', 'name': self.config.name},
  27. {'kind': 'svc', 'name': self.config.name},
  28. {'kind': 'sa', 'name': self.config.config_options['service_account']['value']},
  29. {'kind': 'secret', 'name': self.config.name + '-certs'},
  30. {'kind': 'clusterrolebinding', 'name': 'router-' + self.config.name + '-role'},
  31. ]
  32. self.__prepared_router = None
  33. self.dconfig = None
  34. self.svc = None
  35. self._secret = None
  36. self._serviceaccount = None
  37. self._rolebinding = None
  38. @property
  39. def prepared_router(self):
  40. ''' property for the prepared router'''
  41. if self.__prepared_router is None:
  42. results = self._prepare_router()
  43. if not results or 'returncode' in results and results['returncode'] != 0:
  44. if 'stderr' in results:
  45. raise RouterException('Could not perform router preparation: %s' % results['stderr'])
  46. raise RouterException('Could not perform router preparation.')
  47. self.__prepared_router = results
  48. return self.__prepared_router
  49. @prepared_router.setter
  50. def prepared_router(self, obj):
  51. '''setter for the prepared_router'''
  52. self.__prepared_router = obj
  53. @property
  54. def deploymentconfig(self):
  55. ''' property deploymentconfig'''
  56. return self.dconfig
  57. @deploymentconfig.setter
  58. def deploymentconfig(self, config):
  59. ''' setter for property deploymentconfig '''
  60. self.dconfig = config
  61. @property
  62. def service(self):
  63. ''' property for service '''
  64. return self.svc
  65. @service.setter
  66. def service(self, config):
  67. ''' setter for property service '''
  68. self.svc = config
  69. @property
  70. def secret(self):
  71. ''' property secret '''
  72. return self._secret
  73. @secret.setter
  74. def secret(self, config):
  75. ''' setter for property secret '''
  76. self._secret = config
  77. @property
  78. def serviceaccount(self):
  79. ''' property for serviceaccount '''
  80. return self._serviceaccount
  81. @serviceaccount.setter
  82. def serviceaccount(self, config):
  83. ''' setter for property serviceaccount '''
  84. self._serviceaccount = config
  85. @property
  86. def rolebinding(self):
  87. ''' property rolebinding '''
  88. return self._rolebinding
  89. @rolebinding.setter
  90. def rolebinding(self, config):
  91. ''' setter for property rolebinding '''
  92. self._rolebinding = config
  93. def get_object_by_kind(self, kind):
  94. '''return the current object kind by name'''
  95. if re.match("^(dc|deploymentconfig)$", kind, flags=re.IGNORECASE):
  96. return self.deploymentconfig
  97. elif re.match("^(svc|service)$", kind, flags=re.IGNORECASE):
  98. return self.service
  99. elif re.match("^(sa|serviceaccount)$", kind, flags=re.IGNORECASE):
  100. return self.serviceaccount
  101. elif re.match("secret", kind, flags=re.IGNORECASE):
  102. return self.secret
  103. elif re.match("clusterrolebinding", kind, flags=re.IGNORECASE):
  104. return self.rolebinding
  105. return None
  106. def get(self):
  107. ''' return the self.router_parts '''
  108. self.service = None
  109. self.deploymentconfig = None
  110. self.serviceaccount = None
  111. self.secret = None
  112. self.rolebinding = None
  113. for part in self.router_parts:
  114. result = self._get(part['kind'], name=part['name'])
  115. if result['returncode'] == 0 and part['kind'] == 'dc':
  116. self.deploymentconfig = DeploymentConfig(result['results'][0])
  117. elif result['returncode'] == 0 and part['kind'] == 'svc':
  118. self.service = Service(content=result['results'][0])
  119. elif result['returncode'] == 0 and part['kind'] == 'sa':
  120. self.serviceaccount = ServiceAccount(content=result['results'][0])
  121. elif result['returncode'] == 0 and part['kind'] == 'secret':
  122. self.secret = Secret(content=result['results'][0])
  123. elif result['returncode'] == 0 and part['kind'] == 'clusterrolebinding':
  124. self.rolebinding = RoleBinding(content=result['results'][0])
  125. return {'deploymentconfig': self.deploymentconfig,
  126. 'service': self.service,
  127. 'serviceaccount': self.serviceaccount,
  128. 'secret': self.secret,
  129. 'clusterrolebinding': self.rolebinding,
  130. }
  131. def exists(self):
  132. '''return a whether svc or dc exists '''
  133. if self.deploymentconfig and self.service and self.secret and self.serviceaccount:
  134. return True
  135. return False
  136. def delete(self):
  137. '''return all pods '''
  138. parts = []
  139. for part in self.router_parts:
  140. parts.append(self._delete(part['kind'], part['name']))
  141. rval = 0
  142. for part in parts:
  143. if part['returncode'] != 0 and not 'already exist' in part['stderr']:
  144. rval = part['returncode']
  145. return {'returncode': rval, 'results': parts}
  146. def add_modifications(self, deploymentconfig):
  147. '''modify the deployment config'''
  148. # We want modifications in the form of edits coming in from the module.
  149. # Let's apply these here
  150. # If extended validation is enabled, set the corresponding environment
  151. # variable.
  152. if self.config.config_options['extended_validation']['value']:
  153. if not deploymentconfig.exists_env_key('EXTENDED_VALIDATION'):
  154. deploymentconfig.add_env_value('EXTENDED_VALIDATION', "true")
  155. else:
  156. deploymentconfig.update_env_var('EXTENDED_VALIDATION', "true")
  157. # Apply any edits.
  158. edit_results = []
  159. for edit in self.config.config_options['edits'].get('value', []):
  160. if edit['action'] == 'put':
  161. edit_results.append(deploymentconfig.put(edit['key'],
  162. edit['value']))
  163. if edit['action'] == 'update':
  164. edit_results.append(deploymentconfig.update(edit['key'],
  165. edit['value'],
  166. edit.get('index', None),
  167. edit.get('curr_value', None)))
  168. if edit['action'] == 'append':
  169. edit_results.append(deploymentconfig.append(edit['key'],
  170. edit['value']))
  171. if edit_results and not any([res[0] for res in edit_results]):
  172. return None
  173. return deploymentconfig
  174. # pylint: disable=too-many-branches
  175. def _prepare_router(self):
  176. '''prepare router for instantiation'''
  177. # if cacert, key, and cert were passed, combine them into a pem file
  178. if (self.config.config_options['cacert_file']['value'] and
  179. self.config.config_options['cert_file']['value'] and
  180. self.config.config_options['key_file']['value']):
  181. router_pem = '/tmp/router.pem'
  182. with open(router_pem, 'w') as rfd:
  183. rfd.write(open(self.config.config_options['cert_file']['value']).read())
  184. rfd.write(open(self.config.config_options['key_file']['value']).read())
  185. if self.config.config_options['cacert_file']['value'] and \
  186. os.path.exists(self.config.config_options['cacert_file']['value']):
  187. rfd.write(open(self.config.config_options['cacert_file']['value']).read())
  188. atexit.register(Utils.cleanup, [router_pem])
  189. self.config.config_options['default_cert']['value'] = router_pem
  190. elif self.config.config_options['default_cert']['value'] is None:
  191. # No certificate was passed to us. do not pass one to oc adm router
  192. self.config.config_options['default_cert']['include'] = False
  193. options = self.config.to_option_list(ascommalist='labels')
  194. cmd = ['router', self.config.name]
  195. cmd.extend(options)
  196. cmd.extend(['--dry-run=True', '-o', 'json'])
  197. results = self.openshift_cmd(cmd, oadm=True, output=True, output_type='json')
  198. # pylint: disable=maybe-no-member
  199. if results['returncode'] != 0 or 'items' not in results['results']:
  200. return results
  201. oc_objects = {'DeploymentConfig': {'obj': None, 'path': None, 'update': False},
  202. 'Secret': {'obj': None, 'path': None, 'update': False},
  203. 'ServiceAccount': {'obj': None, 'path': None, 'update': False},
  204. 'ClusterRoleBinding': {'obj': None, 'path': None, 'update': False},
  205. 'Service': {'obj': None, 'path': None, 'update': False},
  206. }
  207. # pylint: disable=invalid-sequence-index
  208. for res in results['results']['items']:
  209. if res['kind'] == 'DeploymentConfig':
  210. oc_objects['DeploymentConfig']['obj'] = DeploymentConfig(res)
  211. elif res['kind'] == 'Service':
  212. oc_objects['Service']['obj'] = Service(res)
  213. elif res['kind'] == 'ServiceAccount':
  214. oc_objects['ServiceAccount']['obj'] = ServiceAccount(res)
  215. elif res['kind'] == 'Secret':
  216. oc_objects['Secret']['obj'] = Secret(res)
  217. elif res['kind'] == 'ClusterRoleBinding':
  218. oc_objects['ClusterRoleBinding']['obj'] = RoleBinding(res)
  219. # Currently only deploymentconfig needs updating
  220. # Verify we got a deploymentconfig
  221. if not oc_objects['DeploymentConfig']['obj']:
  222. return results
  223. # add modifications added
  224. oc_objects['DeploymentConfig']['obj'] = self.add_modifications(oc_objects['DeploymentConfig']['obj'])
  225. for oc_type, oc_data in oc_objects.items():
  226. if oc_data['obj'] is not None:
  227. oc_data['path'] = Utils.create_tmp_file_from_contents(oc_type, oc_data['obj'].yaml_dict)
  228. return oc_objects
  229. def create(self):
  230. '''Create a router
  231. This includes the different parts:
  232. - deploymentconfig
  233. - service
  234. - serviceaccount
  235. - secrets
  236. - clusterrolebinding
  237. '''
  238. results = []
  239. self.needs_update()
  240. # pylint: disable=maybe-no-member
  241. for kind, oc_data in self.prepared_router.items():
  242. if oc_data['obj'] is not None:
  243. time.sleep(1)
  244. if self.get_object_by_kind(kind) is None:
  245. results.append(self._create(oc_data['path']))
  246. elif oc_data['update']:
  247. results.append(self._replace(oc_data['path']))
  248. rval = 0
  249. for result in results:
  250. if result['returncode'] != 0 and not 'already exist' in result['stderr']:
  251. rval = result['returncode']
  252. return {'returncode': rval, 'results': results}
  253. def update(self):
  254. '''run update for the router. This performs a replace'''
  255. results = []
  256. # pylint: disable=maybe-no-member
  257. for _, oc_data in self.prepared_router.items():
  258. if oc_data['update']:
  259. results.append(self._replace(oc_data['path']))
  260. rval = 0
  261. for result in results:
  262. if result['returncode'] != 0:
  263. rval = result['returncode']
  264. return {'returncode': rval, 'results': results}
  265. # pylint: disable=too-many-return-statements,too-many-branches
  266. def needs_update(self):
  267. ''' check to see if we need to update '''
  268. # ServiceAccount:
  269. # Need to determine changes from the pregenerated ones from the original
  270. # Since these are auto generated, we can skip
  271. skip = ['secrets', 'imagePullSecrets']
  272. if self.serviceaccount is None or \
  273. not Utils.check_def_equal(self.prepared_router['ServiceAccount']['obj'].yaml_dict,
  274. self.serviceaccount.yaml_dict,
  275. skip_keys=skip,
  276. debug=self.verbose):
  277. self.prepared_router['ServiceAccount']['update'] = True
  278. # Secret:
  279. # See if one was generated from our dry-run and verify it if needed
  280. if self.prepared_router['Secret']['obj']:
  281. if not self.secret:
  282. self.prepared_router['Secret']['update'] = True
  283. if self.secret is None or \
  284. not Utils.check_def_equal(self.prepared_router['Secret']['obj'].yaml_dict,
  285. self.secret.yaml_dict,
  286. skip_keys=skip,
  287. debug=self.verbose):
  288. self.prepared_router['Secret']['update'] = True
  289. # Service:
  290. # Fix the ports to have protocol=TCP
  291. for port in self.prepared_router['Service']['obj'].get('spec.ports'):
  292. port['protocol'] = 'TCP'
  293. skip = ['portalIP', 'clusterIP', 'sessionAffinity', 'type']
  294. if self.service is None or \
  295. not Utils.check_def_equal(self.prepared_router['Service']['obj'].yaml_dict,
  296. self.service.yaml_dict,
  297. skip_keys=skip,
  298. debug=self.verbose):
  299. self.prepared_router['Service']['update'] = True
  300. # DeploymentConfig:
  301. # Router needs some exceptions.
  302. # We do not want to check the autogenerated password for stats admin
  303. if self.deploymentconfig is not None:
  304. if not self.config.config_options['stats_password']['value']:
  305. for idx, env_var in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\
  306. 'spec.template.spec.containers[0].env') or []):
  307. if env_var['name'] == 'STATS_PASSWORD':
  308. env_var['value'] = \
  309. self.deploymentconfig.get('spec.template.spec.containers[0].env[%s].value' % idx)
  310. break
  311. # dry-run doesn't add the protocol to the ports section. We will manually do that.
  312. for idx, port in enumerate(self.prepared_router['DeploymentConfig']['obj'].get(\
  313. 'spec.template.spec.containers[0].ports') or []):
  314. if not 'protocol' in port:
  315. port['protocol'] = 'TCP'
  316. # These are different when generating
  317. skip = ['dnsPolicy',
  318. 'terminationGracePeriodSeconds',
  319. 'restartPolicy', 'timeoutSeconds',
  320. 'livenessProbe', 'readinessProbe',
  321. 'terminationMessagePath', 'hostPort',
  322. 'defaultMode',
  323. ]
  324. if self.deploymentconfig is None or \
  325. not Utils.check_def_equal(self.prepared_router['DeploymentConfig']['obj'].yaml_dict,
  326. self.deploymentconfig.yaml_dict,
  327. skip_keys=skip,
  328. debug=self.verbose):
  329. self.prepared_router['DeploymentConfig']['update'] = True
  330. # Check if any of the parts need updating, if so, return True
  331. # else, no need to update
  332. # pylint: disable=no-member
  333. return any([self.prepared_router[oc_type]['update'] for oc_type in self.prepared_router.keys()])
  334. @staticmethod
  335. def run_ansible(params, check_mode):
  336. '''run the oc_adm_router module'''
  337. rconfig = RouterConfig(params['name'],
  338. params['namespace'],
  339. params['kubeconfig'],
  340. {'default_cert': {'value': params['default_cert'], 'include': True},
  341. 'cert_file': {'value': params['cert_file'], 'include': False},
  342. 'key_file': {'value': params['key_file'], 'include': False},
  343. 'images': {'value': params['images'], 'include': True},
  344. 'latest_images': {'value': params['latest_images'], 'include': True},
  345. 'labels': {'value': params['labels'], 'include': True},
  346. 'ports': {'value': ','.join(params['ports']), 'include': True},
  347. 'replicas': {'value': params['replicas'], 'include': True},
  348. 'selector': {'value': params['selector'], 'include': True},
  349. 'service_account': {'value': params['service_account'], 'include': True},
  350. 'router_type': {'value': params['router_type'], 'include': False},
  351. 'host_network': {'value': params['host_network'], 'include': True},
  352. 'extended_validation': {'value': params['extended_validation'], 'include': False},
  353. 'external_host': {'value': params['external_host'], 'include': True},
  354. 'external_host_vserver': {'value': params['external_host_vserver'],
  355. 'include': True},
  356. 'external_host_insecure': {'value': params['external_host_insecure'],
  357. 'include': True},
  358. 'external_host_partition_path': {'value': params['external_host_partition_path'],
  359. 'include': True},
  360. 'external_host_username': {'value': params['external_host_username'],
  361. 'include': True},
  362. 'external_host_password': {'value': params['external_host_password'],
  363. 'include': True},
  364. 'external_host_private_key': {'value': params['external_host_private_key'],
  365. 'include': True},
  366. 'stats_user': {'value': params['stats_user'], 'include': True},
  367. 'stats_password': {'value': params['stats_password'], 'include': True},
  368. 'stats_port': {'value': params['stats_port'], 'include': True},
  369. # extra
  370. 'cacert_file': {'value': params['cacert_file'], 'include': False},
  371. # edits
  372. 'edits': {'value': params['edits'], 'include': False},
  373. })
  374. state = params['state']
  375. ocrouter = Router(rconfig, verbose=params['debug'])
  376. api_rval = ocrouter.get()
  377. ########
  378. # get
  379. ########
  380. if state == 'list':
  381. return {'changed': False, 'results': api_rval, 'state': state}
  382. ########
  383. # Delete
  384. ########
  385. if state == 'absent':
  386. if not ocrouter.exists():
  387. return {'changed': False, 'state': state}
  388. if check_mode:
  389. return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a delete.'}
  390. # In case of delete we return a list of each object
  391. # that represents a router and its result in a list
  392. # pylint: disable=redefined-variable-type
  393. api_rval = ocrouter.delete()
  394. return {'changed': True, 'results': api_rval, 'state': state}
  395. if state == 'present':
  396. ########
  397. # Create
  398. ########
  399. if not ocrouter.exists():
  400. if check_mode:
  401. return {'changed': True, 'msg': 'CHECK_MODE: Would have performed a create.'}
  402. api_rval = ocrouter.create()
  403. if api_rval['returncode'] != 0:
  404. return {'failed': True, 'msg': api_rval}
  405. return {'changed': True, 'results': api_rval, 'state': state}
  406. ########
  407. # Update
  408. ########
  409. if not ocrouter.needs_update():
  410. return {'changed': False, 'state': state}
  411. if check_mode:
  412. return {'changed': False, 'msg': 'CHECK_MODE: Would have performed an update.'}
  413. api_rval = ocrouter.update()
  414. if api_rval['returncode'] != 0:
  415. return {'failed': True, 'msg': api_rval}
  416. return {'changed': True, 'results': api_rval, 'state': state}