oc_obj.py 19 KB

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