oc_obj.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. #!/usr/bin/env python
  2. # ___ ___ _ _ ___ ___ _ _____ ___ ___
  3. # / __| __| \| | __| _ \ /_\_ _| __| \
  4. # | (_ | _|| .` | _|| / / _ \| | | _|| |) |
  5. # \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
  6. # | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
  7. # | |) | (_) | | .` | (_) || | | _|| |) | | | |
  8. # |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
  9. '''
  10. OpenShiftCLI class that wraps the oc commands in a subprocess
  11. '''
  12. import atexit
  13. import json
  14. import os
  15. import shutil
  16. import subprocess
  17. import re
  18. import yaml
  19. # This is here because of a bug that causes yaml
  20. # to incorrectly handle timezone info on timestamps
  21. def timestamp_constructor(_, node):
  22. '''return timestamps as strings'''
  23. return str(node.value)
  24. yaml.add_constructor(u'tag:yaml.org,2002:timestamp', timestamp_constructor)
  25. # pylint: disable=too-few-public-methods
  26. class OpenShiftCLI(object):
  27. ''' Class to wrap the oc command line tools '''
  28. def __init__(self,
  29. namespace,
  30. kubeconfig='/etc/origin/master/admin.kubeconfig',
  31. verbose=False):
  32. ''' Constructor for OpenshiftOC '''
  33. self.namespace = namespace
  34. self.verbose = verbose
  35. self.kubeconfig = kubeconfig
  36. # Pylint allows only 5 arguments to be passed.
  37. # pylint: disable=too-many-arguments
  38. def _replace_content(self, resource, rname, content, force=False):
  39. ''' replace the current object with the content '''
  40. res = self._get(resource, rname)
  41. if not res['results']:
  42. return res
  43. fname = '/tmp/%s' % rname
  44. yed = Yedit(fname, res['results'][0])
  45. changes = []
  46. for key, value in content.items():
  47. changes.append(yed.put(key, value))
  48. if any([not change[0] for change in changes]):
  49. return {'returncode': 0, 'updated': False}
  50. yed.write()
  51. atexit.register(Utils.cleanup, [fname])
  52. return self._replace(fname, force)
  53. def _replace(self, fname, force=False):
  54. '''return all pods '''
  55. cmd = ['-n', self.namespace, 'replace', '-f', fname]
  56. if force:
  57. cmd.append('--force')
  58. return self.oc_cmd(cmd)
  59. def _create(self, fname):
  60. '''return all pods '''
  61. return self.oc_cmd(['create', '-f', fname, '-n', self.namespace])
  62. def _delete(self, resource, rname):
  63. '''return all pods '''
  64. return self.oc_cmd(['delete', resource, rname, '-n', self.namespace])
  65. def _get(self, resource, rname=None):
  66. '''return a secret by name '''
  67. cmd = ['get', resource, '-o', 'json', '-n', self.namespace]
  68. if rname:
  69. cmd.append(rname)
  70. rval = self.oc_cmd(cmd, output=True)
  71. # Ensure results are retuned in an array
  72. if rval.has_key('items'):
  73. rval['results'] = rval['items']
  74. elif not isinstance(rval['results'], list):
  75. rval['results'] = [rval['results']]
  76. return rval
  77. def oc_cmd(self, cmd, output=False):
  78. '''Base command for oc '''
  79. #cmds = ['/usr/bin/oc', '--config', self.kubeconfig]
  80. cmds = ['/usr/bin/oc']
  81. cmds.extend(cmd)
  82. rval = {}
  83. results = ''
  84. err = None
  85. if self.verbose:
  86. print ' '.join(cmds)
  87. proc = subprocess.Popen(cmds,
  88. stdout=subprocess.PIPE,
  89. stderr=subprocess.PIPE,
  90. env={'KUBECONFIG': self.kubeconfig})
  91. proc.wait()
  92. stdout = proc.stdout.read()
  93. stderr = proc.stderr.read()
  94. rval = {"returncode": proc.returncode,
  95. "results": results,
  96. }
  97. if proc.returncode == 0:
  98. if output:
  99. try:
  100. rval['results'] = json.loads(stdout)
  101. except ValueError as err:
  102. if "No JSON object could be decoded" in err.message:
  103. err = err.message
  104. if self.verbose:
  105. print stdout
  106. print stderr
  107. print
  108. if err:
  109. rval.update({"err": err,
  110. "stderr": stderr,
  111. "stdout": stdout,
  112. "cmd": cmds
  113. })
  114. else:
  115. rval.update({"stderr": stderr,
  116. "stdout": stdout,
  117. "results": {},
  118. })
  119. return rval
  120. class Utils(object):
  121. ''' utilities for openshiftcli modules '''
  122. @staticmethod
  123. def create_file(rname, data, ftype=None):
  124. ''' create a file in tmp with name and contents'''
  125. path = os.path.join('/tmp', rname)
  126. with open(path, 'w') as fds:
  127. if ftype == 'yaml':
  128. fds.write(yaml.safe_dump(data, default_flow_style=False))
  129. elif ftype == 'json':
  130. fds.write(json.dumps(data))
  131. else:
  132. fds.write(data)
  133. # Register cleanup when module is done
  134. atexit.register(Utils.cleanup, [path])
  135. return path
  136. @staticmethod
  137. def create_files_from_contents(data):
  138. '''Turn an array of dict: filename, content into a files array'''
  139. files = []
  140. for sfile in data:
  141. path = Utils.create_file(sfile['path'], sfile['content'])
  142. files.append(path)
  143. return files
  144. @staticmethod
  145. def cleanup(files):
  146. '''Clean up on exit '''
  147. for sfile in files:
  148. if os.path.exists(sfile):
  149. if os.path.isdir(sfile):
  150. shutil.rmtree(sfile)
  151. elif os.path.isfile(sfile):
  152. os.remove(sfile)
  153. @staticmethod
  154. def exists(results, _name):
  155. ''' Check to see if the results include the name '''
  156. if not results:
  157. return False
  158. if Utils.find_result(results, _name):
  159. return True
  160. return False
  161. @staticmethod
  162. def find_result(results, _name):
  163. ''' Find the specified result by name'''
  164. rval = None
  165. for result in results:
  166. if result.has_key('metadata') and result['metadata']['name'] == _name:
  167. rval = result
  168. break
  169. return rval
  170. @staticmethod
  171. def get_resource_file(sfile, sfile_type='yaml'):
  172. ''' return the service file '''
  173. contents = None
  174. with open(sfile) as sfd:
  175. contents = sfd.read()
  176. if sfile_type == 'yaml':
  177. contents = yaml.safe_load(contents)
  178. elif sfile_type == 'json':
  179. contents = json.loads(contents)
  180. return contents
  181. # Disabling too-many-branches. This is a yaml dictionary comparison function
  182. # pylint: disable=too-many-branches,too-many-return-statements
  183. @staticmethod
  184. def check_def_equal(user_def, result_def, debug=False):
  185. ''' Given a user defined definition, compare it with the results given back by our query. '''
  186. # Currently these values are autogenerated and we do not need to check them
  187. skip = ['metadata', 'status']
  188. for key, value in result_def.items():
  189. if key in skip:
  190. continue
  191. # Both are lists
  192. if isinstance(value, list):
  193. if not isinstance(user_def[key], list):
  194. return False
  195. # lists should be identical
  196. if value != user_def[key]:
  197. return False
  198. # recurse on a dictionary
  199. elif isinstance(value, dict):
  200. if not isinstance(user_def[key], dict):
  201. if debug:
  202. print "dict returned false not instance of dict"
  203. return False
  204. # before passing ensure keys match
  205. api_values = set(value.keys()) - set(skip)
  206. user_values = set(user_def[key].keys()) - set(skip)
  207. if api_values != user_values:
  208. if debug:
  209. print api_values
  210. print user_values
  211. print "keys are not equal in dict"
  212. return False
  213. result = Utils.check_def_equal(user_def[key], value, debug=debug)
  214. if not result:
  215. if debug:
  216. print "dict returned false"
  217. return False
  218. # Verify each key, value pair is the same
  219. else:
  220. if not user_def.has_key(key) or value != user_def[key]:
  221. if debug:
  222. print "value not equal; user_def does not have key"
  223. print value
  224. print user_def[key]
  225. return False
  226. return True
  227. class YeditException(Exception):
  228. ''' Exception class for Yedit '''
  229. pass
  230. class Yedit(object):
  231. ''' Class to modify yaml files '''
  232. re_valid_key = r"(((\[-?\d+\])|(\w+)).?)+$"
  233. re_key = r"(?:\[(-?\d+)\])|(\w+)"
  234. def __init__(self, filename=None, content=None, content_type='yaml'):
  235. self.content = content
  236. self.filename = filename
  237. self.__yaml_dict = content
  238. self.content_type = content_type
  239. if self.filename and not self.content:
  240. self.load(content_type=self.content_type)
  241. @property
  242. def yaml_dict(self):
  243. ''' getter method for yaml_dict '''
  244. return self.__yaml_dict
  245. @yaml_dict.setter
  246. def yaml_dict(self, value):
  247. ''' setter method for yaml_dict '''
  248. self.__yaml_dict = value
  249. @staticmethod
  250. def remove_entry(data, key):
  251. ''' remove data at location key '''
  252. if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))):
  253. return None
  254. key_indexes = re.findall(Yedit.re_key, key)
  255. for arr_ind, dict_key in key_indexes[:-1]:
  256. if dict_key and isinstance(data, dict):
  257. data = data.get(dict_key, None)
  258. elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1:
  259. data = data[int(arr_ind)]
  260. else:
  261. return None
  262. # process last index for remove
  263. # expected list entry
  264. if key_indexes[-1][0]:
  265. if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1:
  266. del data[int(key_indexes[-1][0])]
  267. # expected dict entry
  268. elif key_indexes[-1][1]:
  269. if isinstance(data, dict):
  270. del data[key_indexes[-1][1]]
  271. @staticmethod
  272. def add_entry(data, key, item=None):
  273. ''' Get an item from a dictionary with key notation a.b.c
  274. d = {'a': {'b': 'c'}}}
  275. key = a.b
  276. return c
  277. '''
  278. if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))):
  279. return None
  280. curr_data = data
  281. key_indexes = re.findall(Yedit.re_key, key)
  282. for arr_ind, dict_key in key_indexes[:-1]:
  283. if dict_key:
  284. if isinstance(data, dict) and data.has_key(dict_key):
  285. data = data[dict_key]
  286. continue
  287. data[dict_key] = {}
  288. data = data[dict_key]
  289. elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1:
  290. data = data[int(arr_ind)]
  291. else:
  292. return None
  293. # process last index for add
  294. # expected list entry
  295. if key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1:
  296. data[int(key_indexes[-1][0])] = item
  297. # expected dict entry
  298. elif key_indexes[-1][1] and isinstance(data, dict):
  299. data[key_indexes[-1][1]] = item
  300. return curr_data
  301. @staticmethod
  302. def get_entry(data, key):
  303. ''' Get an item from a dictionary with key notation a.b.c
  304. d = {'a': {'b': 'c'}}}
  305. key = a.b
  306. return c
  307. '''
  308. if not (key and re.match(Yedit.re_valid_key, key) and isinstance(data, (list, dict))):
  309. return None
  310. key_indexes = re.findall(Yedit.re_key, key)
  311. for arr_ind, dict_key in key_indexes:
  312. if dict_key and isinstance(data, dict):
  313. data = data.get(dict_key, None)
  314. elif arr_ind and isinstance(data, list) and int(arr_ind) <= len(data) - 1:
  315. data = data[int(arr_ind)]
  316. else:
  317. return None
  318. return data
  319. def write(self):
  320. ''' write to file '''
  321. if not self.filename:
  322. raise YeditException('Please specify a filename.')
  323. with open(self.filename, 'w') as yfd:
  324. yfd.write(yaml.safe_dump(self.yaml_dict, default_flow_style=False))
  325. def read(self):
  326. ''' write to file '''
  327. # check if it exists
  328. if not self.exists():
  329. return None
  330. contents = None
  331. with open(self.filename) as yfd:
  332. contents = yfd.read()
  333. return contents
  334. def exists(self):
  335. ''' return whether file exists '''
  336. if os.path.exists(self.filename):
  337. return True
  338. return False
  339. def load(self, content_type='yaml'):
  340. ''' return yaml file '''
  341. contents = self.read()
  342. if not contents:
  343. return None
  344. # check if it is yaml
  345. try:
  346. if content_type == 'yaml':
  347. self.yaml_dict = yaml.load(contents)
  348. elif content_type == 'json':
  349. self.yaml_dict = json.loads(contents)
  350. except yaml.YAMLError as _:
  351. # Error loading yaml or json
  352. return None
  353. return self.yaml_dict
  354. def get(self, key):
  355. ''' get a specified key'''
  356. try:
  357. entry = Yedit.get_entry(self.yaml_dict, key)
  358. except KeyError as _:
  359. entry = None
  360. return entry
  361. def delete(self, key):
  362. ''' put key, value into a yaml file '''
  363. try:
  364. entry = Yedit.get_entry(self.yaml_dict, key)
  365. except KeyError as _:
  366. entry = None
  367. if not entry:
  368. return (False, self.yaml_dict)
  369. Yedit.remove_entry(self.yaml_dict, key)
  370. return (True, self.yaml_dict)
  371. def put(self, key, value):
  372. ''' put key, value into a yaml file '''
  373. try:
  374. entry = Yedit.get_entry(self.yaml_dict, key)
  375. except KeyError as _:
  376. entry = None
  377. if entry == value:
  378. return (False, self.yaml_dict)
  379. Yedit.add_entry(self.yaml_dict, key, value)
  380. return (True, self.yaml_dict)
  381. def create(self, key, value):
  382. ''' create the file '''
  383. if not self.exists():
  384. self.yaml_dict = {key: value}
  385. return (True, self.yaml_dict)
  386. return (False, self.yaml_dict)
  387. class OCObject(OpenShiftCLI):
  388. ''' Class to wrap the oc command line tools '''
  389. # pylint allows 5. we need 6
  390. # pylint: disable=too-many-arguments
  391. def __init__(self,
  392. kind,
  393. namespace,
  394. rname=None,
  395. kubeconfig='/etc/origin/master/admin.kubeconfig',
  396. verbose=False):
  397. ''' Constructor for OpenshiftOC '''
  398. super(OCObject, self).__init__(namespace, kubeconfig)
  399. self.kind = kind
  400. self.namespace = namespace
  401. self.name = rname
  402. self.kubeconfig = kubeconfig
  403. self.verbose = verbose
  404. def get(self):
  405. '''return a deploymentconfig by name '''
  406. return self._get(self.kind, rname=self.name)
  407. def delete(self):
  408. '''return all pods '''
  409. return self._delete(self.kind, self.name)
  410. def create(self, files=None, content=None):
  411. '''Create a deploymentconfig '''
  412. if files:
  413. return self._create(files[0])
  414. return self._create(Utils.create_files_from_contents(content))
  415. # pylint: disable=too-many-function-args
  416. def update(self, files=None, content=None, force=False):
  417. '''run update dc
  418. This receives a list of file names and takes the first filename and calls replace.
  419. '''
  420. if files:
  421. return self._replace(files[0], force)
  422. return self.update_content(content, force)
  423. def update_content(self, content, force=False):
  424. '''update the dc with the content'''
  425. return self._replace_content(self.kind, self.name, content, force=force)
  426. def needs_update(self, files=None, content=None, content_type='yaml'):
  427. ''' check to see if we need to update '''
  428. objects = self.get()
  429. if objects['returncode'] != 0:
  430. return objects
  431. # pylint: disable=no-member
  432. data = None
  433. if files:
  434. data = Utils.get_resource_file(files[0], content_type)
  435. # if equal then no need. So not equal is True
  436. return not Utils.check_def_equal(data, objects['results'][0], True)
  437. else:
  438. data = content
  439. for key, value in data.items():
  440. if key == 'metadata':
  441. continue
  442. if not objects['results'][0].has_key(key):
  443. return True
  444. if value != objects['results'][0][key]:
  445. return True
  446. return False
  447. # pylint: disable=too-many-branches
  448. def main():
  449. '''
  450. ansible oc module for services
  451. '''
  452. module = AnsibleModule(
  453. argument_spec=dict(
  454. kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
  455. state=dict(default='present', type='str',
  456. choices=['present', 'absent', 'list']),
  457. debug=dict(default=False, type='bool'),
  458. namespace=dict(default='default', type='str'),
  459. name=dict(default=None, type='str'),
  460. files=dict(default=None, type='list'),
  461. kind=dict(required=True,
  462. type='str',
  463. choices=['dc', 'deploymentconfig',
  464. 'svc', 'service',
  465. 'secret',
  466. ]),
  467. delete_after=dict(default=False, type='bool'),
  468. content=dict(default=None, type='dict'),
  469. force=dict(default=False, type='bool'),
  470. ),
  471. mutually_exclusive=[["content", "files"]],
  472. supports_check_mode=True,
  473. )
  474. ocobj = OCObject(module.params['kind'],
  475. module.params['namespace'],
  476. module.params['name'],
  477. kubeconfig=module.params['kubeconfig'],
  478. verbose=module.params['debug'])
  479. state = module.params['state']
  480. api_rval = ocobj.get()
  481. #####
  482. # Get
  483. #####
  484. if state == 'list':
  485. module.exit_json(changed=False, results=api_rval['results'], state="list")
  486. if not module.params['name']:
  487. module.fail_json(msg='Please specify a name when state is absent|present.')
  488. ########
  489. # Delete
  490. ########
  491. if state == 'absent':
  492. if not Utils.exists(api_rval['results'], module.params['name']):
  493. module.exit_json(changed=False, state="absent")
  494. if module.check_mode:
  495. module.exit_json(change=False, msg='Would have performed a delete.')
  496. api_rval = ocobj.delete()
  497. module.exit_json(changed=True, results=api_rval, state="absent")
  498. if state == 'present':
  499. ########
  500. # Create
  501. ########
  502. if not Utils.exists(api_rval['results'], module.params['name']):
  503. if module.check_mode:
  504. module.exit_json(change=False, msg='Would have performed a create.')
  505. # Create it here
  506. api_rval = ocobj.create(module.params['files'], module.params['content'])
  507. if api_rval['returncode'] != 0:
  508. module.fail_json(msg=api_rval)
  509. # return the created object
  510. api_rval = ocobj.get()
  511. if api_rval['returncode'] != 0:
  512. module.fail_json(msg=api_rval)
  513. # Remove files
  514. if module.params['files'] and module.params['delete_after']:
  515. Utils.cleanup(module.params['files'])
  516. module.exit_json(changed=True, results=api_rval, state="present")
  517. ########
  518. # Update
  519. ########
  520. # if a file path is passed, use it.
  521. update = ocobj.needs_update(module.params['files'], module.params['content'])
  522. if not isinstance(update, bool):
  523. module.fail_json(msg=update)
  524. # No changes
  525. if not update:
  526. if module.params['files'] and module.params['delete_after']:
  527. Utils.cleanup(module.params['files'])
  528. module.exit_json(changed=False, results=api_rval['results'][0], state="present")
  529. if module.check_mode:
  530. module.exit_json(change=False, msg='Would have performed an update.')
  531. api_rval = ocobj.update(module.params['files'],
  532. module.params['content'],
  533. module.params['force'])
  534. if api_rval['returncode'] != 0:
  535. module.fail_json(msg=api_rval)
  536. # return the created object
  537. api_rval = ocobj.get()
  538. if api_rval['returncode'] != 0:
  539. module.fail_json(msg=api_rval)
  540. module.exit_json(changed=True, results=api_rval, state="present")
  541. module.exit_json(failed=True,
  542. changed=False,
  543. results='Unknown state passed. %s' % state,
  544. state="unknown")
  545. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, locally-disabled
  546. # import module snippets. This are required
  547. from ansible.module_utils.basic import *
  548. main()