|
@@ -0,0 +1,379 @@
|
|
|
+#!/usr/bin/env python
|
|
|
+'''
|
|
|
+module for openshift cloud secrets
|
|
|
+'''
|
|
|
+# Examples:
|
|
|
+#
|
|
|
+# # to initiate and use /etc/origin/master/admin.kubeconfig file for auth
|
|
|
+# - name: list secrets
|
|
|
+# oc_secrets:
|
|
|
+# state: list
|
|
|
+# namespace: default
|
|
|
+#
|
|
|
+# # To get a specific secret named 'mysecret'
|
|
|
+# - name: list secrets
|
|
|
+# oc_secrets:
|
|
|
+# state: list
|
|
|
+# namespace: default
|
|
|
+# name: mysecret
|
|
|
+#
|
|
|
+# # To create a secret:
|
|
|
+# # This module expects the user to place the files on the remote server and pass them in.
|
|
|
+# - name: create a secret from file
|
|
|
+# oc_secrets:
|
|
|
+# state: present
|
|
|
+# namespace: default
|
|
|
+# name: mysecret
|
|
|
+# files:
|
|
|
+# - /tmp/config.yml
|
|
|
+# - /tmp/passwords.yml
|
|
|
+# delete_after: False
|
|
|
+
|
|
|
+# # To create a secret:
|
|
|
+# # This module expects the user to place the files on the remote server and pass them in.
|
|
|
+# - name: create a secret from content
|
|
|
+# oc_secrets:
|
|
|
+# state: present
|
|
|
+# namespace: default
|
|
|
+# name: mysecret
|
|
|
+# contents:
|
|
|
+# - path: /tmp/config.yml
|
|
|
+# content: "value=True\n"
|
|
|
+# - path: /tmp/passwords.yml
|
|
|
+# content: "test1\ntest2\ntest3\ntest4\n"
|
|
|
+#
|
|
|
+
|
|
|
+import os
|
|
|
+import shutil
|
|
|
+import json
|
|
|
+import atexit
|
|
|
+
|
|
|
+class OpenShiftOC(object):
|
|
|
+ ''' Class to wrap the oc command line tools
|
|
|
+ '''
|
|
|
+ def __init__(self,
|
|
|
+ namespace,
|
|
|
+ secret_name=None,
|
|
|
+ kubeconfig='/etc/origin/master/admin.kubeconfig',
|
|
|
+ verbose=False):
|
|
|
+ ''' Constructor for OpenshiftOC '''
|
|
|
+ self.namespace = namespace
|
|
|
+ self.name = secret_name
|
|
|
+ self.verbose = verbose
|
|
|
+ self.kubeconfig = kubeconfig
|
|
|
+
|
|
|
+ def get_secrets(self):
|
|
|
+ '''return a secret by name '''
|
|
|
+ cmd = ['get', 'secrets', '-o', 'json', '-n', self.namespace]
|
|
|
+ if self.name:
|
|
|
+ cmd.append(self.name)
|
|
|
+
|
|
|
+ rval = self.oc_cmd(cmd, output=True)
|
|
|
+
|
|
|
+ # Ensure results are retuned in an array
|
|
|
+ if rval.has_key('items'):
|
|
|
+ rval['results'] = rval['items']
|
|
|
+ elif not isinstance(rval['results'], list):
|
|
|
+ rval['results'] = [rval['results']]
|
|
|
+
|
|
|
+ return rval
|
|
|
+
|
|
|
+ def delete_secret(self):
|
|
|
+ '''return all pods '''
|
|
|
+ return self.oc_cmd(['delete', 'secrets', self.name, '-n', self.namespace])
|
|
|
+
|
|
|
+ def secret_new(self, files):
|
|
|
+ '''Create a secret with all pods '''
|
|
|
+ secrets = ["%s=%s" % (os.path.basename(sfile), sfile) for sfile in files]
|
|
|
+ cmd = ['-n%s' % self.namespace, 'secrets', 'new', self.name]
|
|
|
+ cmd.extend(secrets)
|
|
|
+
|
|
|
+ return self.oc_cmd(cmd)
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def create_files_from_contents(data):
|
|
|
+ '''Turn an array of dict: filename, content into a files array'''
|
|
|
+ files = []
|
|
|
+ for sfile in data:
|
|
|
+ with open(sfile['path'], 'w') as fds:
|
|
|
+ fds.write(sfile['content'])
|
|
|
+ files.append(sfile['path'])
|
|
|
+
|
|
|
+ # Register cleanup when module is done
|
|
|
+ atexit.register(OpenShiftOC.cleanup, files)
|
|
|
+ return files
|
|
|
+
|
|
|
+ def update_secret(self, files, force=False):
|
|
|
+ '''run update secret
|
|
|
+
|
|
|
+ This receives a list of file names and converts it into a secret.
|
|
|
+ The secret is then written to disk and passed into the `oc replace` command.
|
|
|
+ '''
|
|
|
+ secret = self.prep_secret(files)
|
|
|
+ if secret['returncode'] != 0:
|
|
|
+ return secret
|
|
|
+
|
|
|
+ sfile_path = '/tmp/%s' % secret['results']['metadata']['name']
|
|
|
+ with open(sfile_path, 'w') as sfd:
|
|
|
+ sfd.write(json.dumps(secret['results']))
|
|
|
+
|
|
|
+ cmd = ['replace', '-f', sfile_path]
|
|
|
+ if force:
|
|
|
+ cmd = ['replace', '--force', '-f', sfile_path]
|
|
|
+
|
|
|
+ atexit.register(OpenShiftOC.cleanup, [sfile_path])
|
|
|
+
|
|
|
+ return self.oc_cmd(cmd)
|
|
|
+
|
|
|
+ def prep_secret(self, files):
|
|
|
+ ''' return what the secret would look like if created
|
|
|
+ This is accomplished by passing -ojson. This will most likely change in the future
|
|
|
+ '''
|
|
|
+ secrets = ["%s=%s" % (os.path.basename(sfile), sfile) for sfile in files]
|
|
|
+ cmd = ['-ojson', '-n%s' % self.namespace, 'secrets', 'new', self.name]
|
|
|
+ cmd.extend(secrets)
|
|
|
+
|
|
|
+ return self.oc_cmd(cmd, output=True)
|
|
|
+
|
|
|
+ def oc_cmd(self, cmd, output=False):
|
|
|
+ '''Base command for oc '''
|
|
|
+ cmds = ['/usr/bin/oc']
|
|
|
+ cmds.extend(cmd)
|
|
|
+
|
|
|
+ results = ''
|
|
|
+
|
|
|
+ if self.verbose:
|
|
|
+ print ' '.join(cmds)
|
|
|
+
|
|
|
+ proc = subprocess.Popen(cmds,
|
|
|
+ stdout=subprocess.PIPE,
|
|
|
+ stderr=subprocess.PIPE,
|
|
|
+ env={'KUBECONFIG': self.kubeconfig})
|
|
|
+ proc.wait()
|
|
|
+ if proc.returncode == 0:
|
|
|
+ if output:
|
|
|
+ try:
|
|
|
+ results = json.loads(proc.stdout.read())
|
|
|
+ except ValueError as err:
|
|
|
+ if "No JSON object could be decoded" in err.message:
|
|
|
+ results = err.message
|
|
|
+
|
|
|
+ if self.verbose:
|
|
|
+ print proc.stderr.read()
|
|
|
+ print results
|
|
|
+ print
|
|
|
+
|
|
|
+ return {"returncode": proc.returncode, "results": results}
|
|
|
+
|
|
|
+ return {"returncode": proc.returncode,
|
|
|
+ "stderr": proc.stderr.read(),
|
|
|
+ "stdout": proc.stdout.read(),
|
|
|
+ "results": {}
|
|
|
+ }
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def cleanup(files):
|
|
|
+ '''Clean up on exit '''
|
|
|
+ for sfile in files:
|
|
|
+ if os.path.exists(sfile):
|
|
|
+ if os.path.isdir(sfile):
|
|
|
+ shutil.rmtree(sfile)
|
|
|
+ elif os.path.isfile(sfile):
|
|
|
+ os.remove(sfile)
|
|
|
+
|
|
|
+
|
|
|
+def exists(results, _name):
|
|
|
+ ''' Check to see if the results include the name '''
|
|
|
+ if not results:
|
|
|
+ return False
|
|
|
+
|
|
|
+ if find_result(results, _name):
|
|
|
+ return True
|
|
|
+
|
|
|
+ return False
|
|
|
+
|
|
|
+def find_result(results, _name):
|
|
|
+ ''' Find the specified result by name'''
|
|
|
+ rval = None
|
|
|
+ for result in results:
|
|
|
+ #print "%s == %s" % (result['metadata']['name'], name)
|
|
|
+ if result.has_key('metadata') and result['metadata']['name'] == _name:
|
|
|
+ rval = result
|
|
|
+ break
|
|
|
+
|
|
|
+ return rval
|
|
|
+
|
|
|
+# Disabling too-many-branches. This is a yaml dictionary comparison function
|
|
|
+# pylint: disable=too-many-branches,too-many-return-statements
|
|
|
+def check_def_equal(user_def, result_def, debug=False):
|
|
|
+ ''' Given a user defined definition, compare it with the results given back by our query. '''
|
|
|
+
|
|
|
+ # Currently these values are autogenerated and we do not need to check them
|
|
|
+ skip = ['creationTimestamp', 'selfLink', 'resourceVersion', 'uid', 'namespace']
|
|
|
+
|
|
|
+ for key, value in result_def.items():
|
|
|
+ if key in skip:
|
|
|
+ continue
|
|
|
+
|
|
|
+ # Both are lists
|
|
|
+ if isinstance(value, list):
|
|
|
+ if not isinstance(user_def[key], list):
|
|
|
+ return False
|
|
|
+
|
|
|
+ # lists should be identical
|
|
|
+ if value != user_def[key]:
|
|
|
+ return False
|
|
|
+
|
|
|
+ # recurse on a dictionary
|
|
|
+ elif isinstance(value, dict):
|
|
|
+ if not isinstance(user_def[key], dict):
|
|
|
+ if debug:
|
|
|
+ print "dict returned false not instance of dict"
|
|
|
+ return False
|
|
|
+
|
|
|
+ # before passing ensure keys match
|
|
|
+ api_values = set(value.keys()) - set(skip)
|
|
|
+ user_values = set(user_def[key].keys()) - set(skip)
|
|
|
+ if api_values != user_values:
|
|
|
+ if debug:
|
|
|
+ print api_values
|
|
|
+ print user_values
|
|
|
+ print "keys are not equal in dict"
|
|
|
+ return False
|
|
|
+
|
|
|
+ result = check_def_equal(user_def[key], value)
|
|
|
+ if not result:
|
|
|
+ if debug:
|
|
|
+ print "dict returned false"
|
|
|
+ return False
|
|
|
+
|
|
|
+ # Verify each key, value pair is the same
|
|
|
+ else:
|
|
|
+ if not user_def.has_key(key) or value != user_def[key]:
|
|
|
+ if debug:
|
|
|
+ print "value not equal; user_def does not have key"
|
|
|
+ print value
|
|
|
+ print user_def[key]
|
|
|
+ return False
|
|
|
+
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
+def main():
|
|
|
+ '''
|
|
|
+ ansible oc module for secrets
|
|
|
+ '''
|
|
|
+
|
|
|
+ module = AnsibleModule(
|
|
|
+ argument_spec=dict(
|
|
|
+ kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
|
|
|
+ state=dict(default='present', type='str',
|
|
|
+ choices=['present', 'absent', 'list']),
|
|
|
+ debug=dict(default=False, type='bool'),
|
|
|
+ namespace=dict(default='default', type='str'),
|
|
|
+ name=dict(default=None, type='str'),
|
|
|
+ files=dict(default=None, type='list'),
|
|
|
+ delete_after=dict(default=False, type='bool'),
|
|
|
+ contents=dict(default=None, type='list'),
|
|
|
+ force=dict(default=False, type='bool'),
|
|
|
+ ),
|
|
|
+ mutually_exclusive=[["contents", "files"]],
|
|
|
+
|
|
|
+ supports_check_mode=True,
|
|
|
+ )
|
|
|
+ occmd = OpenShiftOC(module.params['namespace'],
|
|
|
+ module.params['name'],
|
|
|
+ kubeconfig=module.params['kubeconfig'],
|
|
|
+ verbose=module.params['debug'])
|
|
|
+
|
|
|
+ state = module.params['state']
|
|
|
+
|
|
|
+ api_rval = occmd.get_secrets()
|
|
|
+
|
|
|
+ #####
|
|
|
+ # Get
|
|
|
+ #####
|
|
|
+ if state == 'list':
|
|
|
+ module.exit_json(changed=False, results=api_rval['results'], state="list")
|
|
|
+
|
|
|
+ if not module.params['name']:
|
|
|
+ module.fail_json(msg='Please specify a name when state is absent|present.')
|
|
|
+ ########
|
|
|
+ # Delete
|
|
|
+ ########
|
|
|
+ if state == 'absent':
|
|
|
+ if not exists(api_rval['results'], module.params['name']):
|
|
|
+ module.exit_json(changed=False, state="absent")
|
|
|
+
|
|
|
+ if module.check_mode:
|
|
|
+ module.exit_json(change=False, msg='Would have performed a delete.')
|
|
|
+
|
|
|
+ api_rval = occmd.delete_secret()
|
|
|
+ module.exit_json(changed=True, results=api_rval, state="absent")
|
|
|
+
|
|
|
+
|
|
|
+ if state == 'present':
|
|
|
+ if module.params['files']:
|
|
|
+ files = module.params['files']
|
|
|
+ elif module.params['contents']:
|
|
|
+ files = OpenShiftOC.create_files_from_contents(module.params['contents'])
|
|
|
+ else:
|
|
|
+ module.fail_json(msg='Either specify files or contents.')
|
|
|
+
|
|
|
+ ########
|
|
|
+ # Create
|
|
|
+ ########
|
|
|
+ if not exists(api_rval['results'], module.params['name']):
|
|
|
+
|
|
|
+ if module.check_mode:
|
|
|
+ module.exit_json(change=False, msg='Would have performed a create.')
|
|
|
+
|
|
|
+ api_rval = occmd.secret_new(files)
|
|
|
+
|
|
|
+ # Remove files
|
|
|
+ if files and module.params['delete_after']:
|
|
|
+ OpenShiftOC.cleanup(files)
|
|
|
+
|
|
|
+ module.exit_json(changed=True, results=api_rval, state="present")
|
|
|
+
|
|
|
+ ########
|
|
|
+ # Update
|
|
|
+ ########
|
|
|
+ secret = occmd.prep_secret(files)
|
|
|
+
|
|
|
+ if secret['returncode'] != 0:
|
|
|
+ module.fail_json(msg=secret)
|
|
|
+
|
|
|
+ if check_def_equal(secret['results'], api_rval['results'][0]):
|
|
|
+
|
|
|
+ # Remove files
|
|
|
+ if files and module.params['delete_after']:
|
|
|
+ OpenShiftOC.cleanup(files)
|
|
|
+
|
|
|
+ module.exit_json(changed=False, results=secret['results'], state="present")
|
|
|
+
|
|
|
+ if module.check_mode:
|
|
|
+ module.exit_json(change=False, msg='Would have performed an update.')
|
|
|
+
|
|
|
+ api_rval = occmd.update_secret(files, force=module.params['force'])
|
|
|
+
|
|
|
+ # Remove files
|
|
|
+ if files and module.params['delete_after']:
|
|
|
+ OpenShiftOC.cleanup(files)
|
|
|
+
|
|
|
+ if api_rval['returncode'] != 0:
|
|
|
+ module.fail_json(msg=api_rval)
|
|
|
+
|
|
|
+
|
|
|
+ module.exit_json(changed=True, results=api_rval, state="present")
|
|
|
+
|
|
|
+ module.exit_json(failed=True,
|
|
|
+ changed=False,
|
|
|
+ results='Unknown state passed. %s' % state,
|
|
|
+ state="unknown")
|
|
|
+
|
|
|
+# pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import, locally-disabled
|
|
|
+# import module snippets. This are required
|
|
|
+from ansible.module_utils.basic import *
|
|
|
+
|
|
|
+main()
|