oc_secrets.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. #!/usr/bin/env python
  2. '''
  3. module for openshift cloud secrets
  4. '''
  5. # Examples:
  6. #
  7. # # to initiate and use /etc/origin/master/admin.kubeconfig file for auth
  8. # - name: list secrets
  9. # oc_secrets:
  10. # state: list
  11. # namespace: default
  12. #
  13. # # To get a specific secret named 'mysecret'
  14. # - name: list secrets
  15. # oc_secrets:
  16. # state: list
  17. # namespace: default
  18. # name: mysecret
  19. #
  20. # # To create a secret:
  21. # # This module expects the user to place the files on the remote server and pass them in.
  22. # - name: create a secret from file
  23. # oc_secrets:
  24. # state: present
  25. # namespace: default
  26. # name: mysecret
  27. # files:
  28. # - /tmp/config.yml
  29. # - /tmp/passwords.yml
  30. # delete_after: False
  31. # # To create a secret:
  32. # # This module expects the user to place the files on the remote server and pass them in.
  33. # - name: create a secret from content
  34. # oc_secrets:
  35. # state: present
  36. # namespace: default
  37. # name: mysecret
  38. # contents:
  39. # - path: /tmp/config.yml
  40. # content: "value=True\n"
  41. # - path: /tmp/passwords.yml
  42. # content: "test1\ntest2\ntest3\ntest4\n"
  43. #
  44. import os
  45. import shutil
  46. import json
  47. import atexit
  48. class OpenShiftOC(object):
  49. ''' Class to wrap the oc command line tools
  50. '''
  51. def __init__(self,
  52. namespace,
  53. secret_name=None,
  54. kubeconfig='/etc/origin/master/admin.kubeconfig',
  55. verbose=False):
  56. ''' Constructor for OpenshiftOC '''
  57. self.namespace = namespace
  58. self.name = secret_name
  59. self.verbose = verbose
  60. self.kubeconfig = kubeconfig
  61. def get_secrets(self):
  62. '''return a secret by name '''
  63. cmd = ['get', 'secrets', '-o', 'json', '-n', self.namespace]
  64. if self.name:
  65. cmd.append(self.name)
  66. rval = self.oc_cmd(cmd, output=True)
  67. # Ensure results are retuned in an array
  68. if rval.has_key('items'):
  69. rval['results'] = rval['items']
  70. elif not isinstance(rval['results'], list):
  71. rval['results'] = [rval['results']]
  72. return rval
  73. def delete_secret(self):
  74. '''return all pods '''
  75. return self.oc_cmd(['delete', 'secrets', self.name, '-n', self.namespace])
  76. def secret_new(self, files):
  77. '''Create a secret with all pods '''
  78. secrets = ["%s=%s" % (os.path.basename(sfile), sfile) for sfile in files]
  79. cmd = ['-n%s' % self.namespace, 'secrets', 'new', self.name]
  80. cmd.extend(secrets)
  81. return self.oc_cmd(cmd)
  82. @staticmethod
  83. def create_files_from_contents(data):
  84. '''Turn an array of dict: filename, content into a files array'''
  85. files = []
  86. for sfile in data:
  87. with open(sfile['path'], 'w') as fds:
  88. fds.write(sfile['content'])
  89. files.append(sfile['path'])
  90. # Register cleanup when module is done
  91. atexit.register(OpenShiftOC.cleanup, files)
  92. return files
  93. def update_secret(self, files, force=False):
  94. '''run update secret
  95. This receives a list of file names and converts it into a secret.
  96. The secret is then written to disk and passed into the `oc replace` command.
  97. '''
  98. secret = self.prep_secret(files)
  99. if secret['returncode'] != 0:
  100. return secret
  101. sfile_path = '/tmp/%s' % secret['results']['metadata']['name']
  102. with open(sfile_path, 'w') as sfd:
  103. sfd.write(json.dumps(secret['results']))
  104. cmd = ['replace', '-f', sfile_path]
  105. if force:
  106. cmd = ['replace', '--force', '-f', sfile_path]
  107. atexit.register(OpenShiftOC.cleanup, [sfile_path])
  108. return self.oc_cmd(cmd)
  109. def prep_secret(self, files):
  110. ''' return what the secret would look like if created
  111. This is accomplished by passing -ojson. This will most likely change in the future
  112. '''
  113. secrets = ["%s=%s" % (os.path.basename(sfile), sfile) for sfile in files]
  114. cmd = ['-ojson', '-n%s' % self.namespace, 'secrets', 'new', self.name]
  115. cmd.extend(secrets)
  116. return self.oc_cmd(cmd, output=True)
  117. def oc_cmd(self, cmd, output=False):
  118. '''Base command for oc '''
  119. cmds = ['/usr/bin/oc']
  120. cmds.extend(cmd)
  121. results = ''
  122. if self.verbose:
  123. print ' '.join(cmds)
  124. proc = subprocess.Popen(cmds,
  125. stdout=subprocess.PIPE,
  126. stderr=subprocess.PIPE,
  127. env={'KUBECONFIG': self.kubeconfig})
  128. proc.wait()
  129. if proc.returncode == 0:
  130. if output:
  131. try:
  132. results = json.loads(proc.stdout.read())
  133. except ValueError as err:
  134. if "No JSON object could be decoded" in err.message:
  135. results = err.message
  136. if self.verbose:
  137. print proc.stderr.read()
  138. print results
  139. print
  140. return {"returncode": proc.returncode, "results": results}
  141. return {"returncode": proc.returncode,
  142. "stderr": proc.stderr.read(),
  143. "stdout": proc.stdout.read(),
  144. "results": {}
  145. }
  146. @staticmethod
  147. def cleanup(files):
  148. '''Clean up on exit '''
  149. for sfile in files:
  150. if os.path.exists(sfile):
  151. if os.path.isdir(sfile):
  152. shutil.rmtree(sfile)
  153. elif os.path.isfile(sfile):
  154. os.remove(sfile)
  155. def exists(results, _name):
  156. ''' Check to see if the results include the name '''
  157. if not results:
  158. return False
  159. if find_result(results, _name):
  160. return True
  161. return False
  162. def find_result(results, _name):
  163. ''' Find the specified result by name'''
  164. rval = None
  165. for result in results:
  166. #print "%s == %s" % (result['metadata']['name'], name)
  167. if result.has_key('metadata') and result['metadata']['name'] == _name:
  168. rval = result
  169. break
  170. return rval
  171. # Disabling too-many-branches. This is a yaml dictionary comparison function
  172. # pylint: disable=too-many-branches,too-many-return-statements
  173. def check_def_equal(user_def, result_def, debug=False):
  174. ''' Given a user defined definition, compare it with the results given back by our query. '''
  175. # Currently these values are autogenerated and we do not need to check them
  176. skip = ['creationTimestamp', 'selfLink', 'resourceVersion', 'uid', 'namespace']
  177. for key, value in result_def.items():
  178. if key in skip:
  179. continue
  180. # Both are lists
  181. if isinstance(value, list):
  182. if not isinstance(user_def[key], list):
  183. return False
  184. # lists should be identical
  185. if value != user_def[key]:
  186. return False
  187. # recurse on a dictionary
  188. elif isinstance(value, dict):
  189. if not isinstance(user_def[key], dict):
  190. if debug:
  191. print "dict returned false not instance of dict"
  192. return False
  193. # before passing ensure keys match
  194. api_values = set(value.keys()) - set(skip)
  195. user_values = set(user_def[key].keys()) - set(skip)
  196. if api_values != user_values:
  197. if debug:
  198. print api_values
  199. print user_values
  200. print "keys are not equal in dict"
  201. return False
  202. result = check_def_equal(user_def[key], value)
  203. if not result:
  204. if debug:
  205. print "dict returned false"
  206. return False
  207. # Verify each key, value pair is the same
  208. else:
  209. if not user_def.has_key(key) or value != user_def[key]:
  210. if debug:
  211. print "value not equal; user_def does not have key"
  212. print value
  213. print user_def[key]
  214. return False
  215. return True
  216. def main():
  217. '''
  218. ansible oc module for secrets
  219. '''
  220. module = AnsibleModule(
  221. argument_spec=dict(
  222. kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
  223. state=dict(default='present', type='str',
  224. choices=['present', 'absent', 'list']),
  225. debug=dict(default=False, type='bool'),
  226. namespace=dict(default='default', type='str'),
  227. name=dict(default=None, type='str'),
  228. files=dict(default=None, type='list'),
  229. delete_after=dict(default=False, type='bool'),
  230. contents=dict(default=None, type='list'),
  231. force=dict(default=False, type='bool'),
  232. ),
  233. mutually_exclusive=[["contents", "files"]],
  234. supports_check_mode=True,
  235. )
  236. occmd = OpenShiftOC(module.params['namespace'],
  237. module.params['name'],
  238. kubeconfig=module.params['kubeconfig'],
  239. verbose=module.params['debug'])
  240. state = module.params['state']
  241. api_rval = occmd.get_secrets()
  242. #####
  243. # Get
  244. #####
  245. if state == 'list':
  246. module.exit_json(changed=False, results=api_rval['results'], state="list")
  247. if not module.params['name']:
  248. module.fail_json(msg='Please specify a name when state is absent|present.')
  249. ########
  250. # Delete
  251. ########
  252. if state == 'absent':
  253. if not exists(api_rval['results'], module.params['name']):
  254. module.exit_json(changed=False, state="absent")
  255. if module.check_mode:
  256. module.exit_json(change=False, msg='Would have performed a delete.')
  257. api_rval = occmd.delete_secret()
  258. module.exit_json(changed=True, results=api_rval, state="absent")
  259. if state == 'present':
  260. if module.params['files']:
  261. files = module.params['files']
  262. elif module.params['contents']:
  263. files = OpenShiftOC.create_files_from_contents(module.params['contents'])
  264. else:
  265. module.fail_json(msg='Either specify files or contents.')
  266. ########
  267. # Create
  268. ########
  269. if not exists(api_rval['results'], module.params['name']):
  270. if module.check_mode:
  271. module.exit_json(change=False, msg='Would have performed a create.')
  272. api_rval = occmd.secret_new(files)
  273. # Remove files
  274. if files and module.params['delete_after']:
  275. OpenShiftOC.cleanup(files)
  276. module.exit_json(changed=True, results=api_rval, state="present")
  277. ########
  278. # Update
  279. ########
  280. secret = occmd.prep_secret(files)
  281. if secret['returncode'] != 0:
  282. module.fail_json(msg=secret)
  283. if check_def_equal(secret['results'], api_rval['results'][0]):
  284. # Remove files
  285. if files and module.params['delete_after']:
  286. OpenShiftOC.cleanup(files)
  287. module.exit_json(changed=False, results=secret['results'], state="present")
  288. if module.check_mode:
  289. module.exit_json(change=False, msg='Would have performed an update.')
  290. api_rval = occmd.update_secret(files, force=module.params['force'])
  291. # Remove files
  292. if files and module.params['delete_after']:
  293. OpenShiftOC.cleanup(files)
  294. if api_rval['returncode'] != 0:
  295. module.fail_json(msg=api_rval)
  296. module.exit_json(changed=True, results=api_rval, state="present")
  297. module.exit_json(failed=True,
  298. changed=False,
  299. results='Unknown state passed. %s' % state,
  300. state="unknown")
  301. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, locally-disabled
  302. # import module snippets. This are required
  303. from ansible.module_utils.basic import *
  304. main()