diff --git a/README.md b/README.md
index a8429905..d2fc0516 100644
--- a/README.md
+++ b/README.md
@@ -8,9 +8,11 @@ The modules are found in the [library folder](./library). Each has full document
If you find an issue with a particular module, or have suggestions, please file an issue at the [OpenShift Rest Client repo](https://github.com/openshift/openshift-restclient-python/issues).
+For convenience, the `k8s_common.py` module is included under [module_utils](./module_utils). This is because it is not currenlty part of an official Ansible release. It is part of Ansible, and you will find it in the `devel` branch. So at some point in the future, it will be part of a relase. In the meantime, it will also be included here. If you happen to find a bug, or would like to make a change, please open issue ans submit pull requests at the [Ansible repo](https://github.com/ansible/ansible).
+
## Requirements
-- Ansible installed from source
+- Ansible
- [OpenShift Rest Client](https://github.com/openshift/openshift-restclient-python) installed on the host where the modules will execute.
## Installation and use
diff --git a/module_utils/README.md b/module_utils/README.md
new file mode 100644
index 00000000..53a6c9f9
--- /dev/null
+++ b/module_utils/README.md
@@ -0,0 +1,3 @@
+The `k8_common.py` module is not currently available in an official release of Ansible. It is part of Ansible, and you'll find it on the `devel` branch, so at some point it will make it into a release. Until then, it's included here in order for convenience.
+
+If you have uncovered a problem with it, or would like to make a change, please open issue and submit pull requess at the [Ansible repo](https://github.com/ansible/ansible).
diff --git a/module_utils/k8s_common.py b/module_utils/k8s_common.py
new file mode 100644
index 00000000..7800898f
--- /dev/null
+++ b/module_utils/k8s_common.py
@@ -0,0 +1,313 @@
+#
+# Copyright 2017 Red Hat | Ansible
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+
+import copy
+import json
+import os
+
+from ansible.module_utils.basic import AnsibleModule
+
+try:
+ from openshift.helper.ansible import KubernetesAnsibleModuleHelper, ARG_ATTRIBUTES_BLACKLIST
+ from openshift.helper.exceptions import KubernetesException
+ HAS_K8S_MODULE_HELPER = True
+except ImportError as exc:
+ HAS_K8S_MODULE_HELPER = False
+
+try:
+ import yaml
+ HAS_YAML = True
+except ImportError:
+ HAS_YAML = False
+
+
+class KubernetesAnsibleException(Exception):
+ pass
+
+
+class KubernetesAnsibleModule(AnsibleModule):
+ @staticmethod
+ def get_helper(api_version, kind):
+ return KubernetesAnsibleModuleHelper(api_version, kind)
+
+ def __init__(self, kind, api_version):
+ self.api_version = api_version
+ self.kind = kind
+ self.argspec_cache = None
+
+ if not HAS_K8S_MODULE_HELPER:
+ raise KubernetesAnsibleException(
+ "This module requires the OpenShift Python client. Try `pip install openshift`"
+ )
+
+ if not HAS_YAML:
+ raise KubernetesAnsibleException(
+ "This module requires PyYAML. Try `pip install PyYAML`"
+ )
+
+ try:
+ self.helper = self.get_helper(api_version, kind)
+ except Exception as exc:
+ raise KubernetesAnsibleException(
+ "Error initializing AnsibleModuleHelper: {}".format(exc)
+ )
+
+ mutually_exclusive = (
+ ('resource_definition', 'src'),
+ )
+
+ AnsibleModule.__init__(self,
+ argument_spec=self.argspec,
+ supports_check_mode=True,
+ mutually_exclusive=mutually_exclusive)
+
+ @property
+ def argspec(self):
+ """
+ Build the module argument spec from the helper.argspec, removing any extra attributes not needed by
+ Ansible.
+
+ :return: dict: a valid Ansible argument spec
+ """
+ if not self.argspec_cache:
+ spec = {
+ 'dry_run': {
+ 'type': 'bool',
+ 'default': False,
+ 'description': [
+ "If set to C(True) the module will exit without executing any action."
+ "Useful to only generate YAML file definitions for the resources in the tasks."
+ ]
+ }
+ }
+
+ for arg_name, arg_properties in self.helper.argspec.items():
+ spec[arg_name] = {}
+ for option, option_value in arg_properties.items():
+ if option not in ARG_ATTRIBUTES_BLACKLIST:
+ if option == 'choices':
+ if isinstance(option_value, dict):
+ spec[arg_name]['choices'] = [value for key, value in option_value.items()]
+ else:
+ spec[arg_name]['choices'] = option_value
+ else:
+ spec[arg_name][option] = option_value
+
+ self.argspec_cache = spec
+ return self.argspec_cache
+
+ def execute_module(self):
+ """
+ Performs basic CRUD operations on the model object. Ends by calling
+ AnsibleModule.fail_json(), if an error is encountered, otherwise
+ AnsibleModule.exit_json() with a dict containing:
+ changed: boolean
+ api_version: the API version
+ : a dict representing the object's state
+ :return: None
+ """
+
+ if self.params.get('debug'):
+ self.helper.enable_debug(reset_logfile=False)
+ self.helper.log_argspec()
+
+ resource_definition = self.params.get('resource_definition')
+ if self.params.get('src'):
+ resource_definition = self.load_resource_definition(self.params['src'])
+ if resource_definition:
+ resource_params = self.resource_to_parameters(resource_definition)
+ self.params.update(resource_params)
+
+ state = self.params.get('state', None)
+ force = self.params.get('force', False)
+ dry_run = self.params.pop('dry_run', False)
+ name = self.params.get('name')
+ namespace = self.params.get('namespace', None)
+ existing = None
+
+ return_attributes = dict(changed=False,
+ api_version=self.api_version,
+ request=self.helper.request_body_from_params(self.params))
+ return_attributes[self.helper.base_model_name_snake] = {}
+
+ if dry_run:
+ self.exit_json(**return_attributes)
+
+ try:
+ auth_options = {}
+ for key, value in self.helper.argspec.items():
+ if value.get('auth_option') and self.params.get(key) is not None:
+ auth_options[key] = self.params[key]
+ self.helper.set_client_config(**auth_options)
+ except KubernetesException as e:
+ self.fail_json(msg='Error loading config', error=str(e))
+
+ if state is None:
+ # This is a list, rollback or ? module with no 'state' param
+ if self.helper.base_model_name_snake.endswith('list'):
+ # For list modules, execute a GET, and exit
+ k8s_obj = self._read(name, namespace)
+ return_attributes[self.kind] = k8s_obj.to_dict()
+ self.exit_json(**return_attributes)
+ elif self.helper.has_method('create'):
+ # For a rollback, execute a POST, and exit
+ k8s_obj = self._create(namespace)
+ return_attributes[self.kind] = k8s_obj.to_dict()
+ return_attributes['changed'] = True
+ self.exit_json(**return_attributes)
+ else:
+ self.fail_json(msg="Missing state parameter. Expected one of: present, absent")
+
+ # CRUD modules
+ try:
+ existing = self.helper.get_object(name, namespace)
+ except KubernetesException as exc:
+ self.fail_json(msg='Failed to retrieve requested object: {}'.format(exc.message),
+ error=exc.value.get('status'))
+
+ if state == 'absent':
+ if not existing:
+ # The object already does not exist
+ self.exit_json(**return_attributes)
+ else:
+ # Delete the object
+ if not self.check_mode:
+ try:
+ self.helper.delete_object(name, namespace)
+ except KubernetesException as exc:
+ self.fail_json(msg="Failed to delete object: {}".format(exc.message),
+ error=exc.value.get('status'))
+ return_attributes['changed'] = True
+ self.exit_json(**return_attributes)
+ else:
+ if not existing:
+ k8s_obj = self._create(namespace)
+ return_attributes[self.kind] = k8s_obj.to_dict()
+ return_attributes['changed'] = True
+ self.exit_json(**return_attributes)
+
+ if existing and force:
+ k8s_obj = None
+ request_body = self.helper.request_body_from_params(self.params)
+ if not self.check_mode:
+ try:
+ k8s_obj = self.helper.replace_object(name, namespace, body=request_body)
+ except KubernetesException as exc:
+ self.fail_json(msg="Failed to replace object: {}".format(exc.message),
+ error=exc.value.get('status'))
+ return_attributes[self.kind] = k8s_obj.to_dict()
+ return_attributes['changed'] = True
+ self.exit_json(**return_attributes)
+
+ # Check if existing object should be patched
+ k8s_obj = copy.deepcopy(existing)
+ try:
+ self.helper.object_from_params(self.params, obj=k8s_obj)
+ except KubernetesException as exc:
+ self.fail_json(msg="Failed to patch object: {}".format(exc.message))
+ match, diff = self.helper.objects_match(existing, k8s_obj)
+ if match:
+ return_attributes[self.kind] = existing.to_dict()
+ self.exit_json(**return_attributes)
+ else:
+ self.helper.log('Existing:')
+ self.helper.log(json.dumps(existing.to_dict(), indent=4))
+ self.helper.log('\nDifferences:')
+ self.helper.log(json.dumps(diff, indent=4))
+ # Differences exist between the existing obj and requested params
+ if not self.check_mode:
+ try:
+ k8s_obj = self.helper.patch_object(name, namespace, k8s_obj)
+ except KubernetesException as exc:
+ self.fail_json(msg="Failed to patch object: {}".format(exc.message))
+ return_attributes[self.kind] = k8s_obj.to_dict()
+ return_attributes['changed'] = True
+ self.exit_json(**return_attributes)
+
+ def _create(self, namespace):
+ request_body = None
+ k8s_obj = None
+ try:
+ request_body = self.helper.request_body_from_params(self.params)
+ except KubernetesException as exc:
+ self.fail_json(msg="Failed to create object: {}".format(exc.message))
+ if not self.check_mode:
+ try:
+ k8s_obj = self.helper.create_object(namespace, body=request_body)
+ except KubernetesException as exc:
+ self.fail_json(msg="Failed to create object: {}".format(exc.message),
+ error=exc.value.get('status'))
+ return k8s_obj
+
+ def _read(self, name, namespace):
+ k8s_obj = None
+ try:
+ k8s_obj = self.helper.get_object(name, namespace)
+ except KubernetesException as exc:
+ self.fail_json(msg='Failed to retrieve requested object',
+ error=exc.value.get('status'))
+ return k8s_obj
+
+ def load_resource_definition(self, src):
+ """ Load the requested src path """
+ result = None
+ path = os.path.normpath(src)
+ self.helper.log("Reading definition from {}".format(path))
+ if not os.path.exists(path):
+ self.fail_json(msg="Error accessing {}. Does the file exist?".format(path))
+ try:
+ result = yaml.safe_load(open(path, 'r'))
+ except (IOError, yaml.YAMLError) as exc:
+ self.fail_json(msg="Error loading resource_definition: {}".format(exc))
+ return result
+
+ def resource_to_parameters(self, resource):
+ """ Converts a resource definition to module parameters """
+ parameters = {}
+ for key, value in resource.items():
+ if key in ('apiVersion', 'kind', 'status'):
+ continue
+ elif key == 'metadata' and isinstance(value, dict):
+ for meta_key, meta_value in value.items():
+ if meta_key in ('name', 'namespace', 'labels', 'annotations'):
+ parameters[meta_key] = meta_value
+ elif key in self.helper.argspec and value is not None:
+ parameters[key] = value
+ elif isinstance(value, dict):
+ self._add_parameter(value, [key], parameters)
+ self.helper.log("Request to parameters: {}".format(json.dumps(parameters)))
+ return parameters
+
+ def _add_parameter(self, request, path, parameters):
+ for key, value in request.items():
+ if path:
+ param_name = '_'.join(path + [self.helper.attribute_to_snake(key)])
+ else:
+ param_name = self.helper.attribute_to_snake(key)
+ if param_name in self.helper.argspec and value is not None:
+ parameters[param_name] = value
+ elif isinstance(value, dict):
+ continue_path = copy.copy(path) if path else []
+ continue_path.append(self.helper.attribute_to_snake(key))
+ self._add_parameter(value, continue_path, parameters)
+ else:
+ self.fail_json(
+ msg=("Error parsing resource definition. Encountered {}, which does not map to a module "
+ "parameter. If this looks like a problem with the module, please open an issue at "
+ "github.com/openshift/openshift-restclient-python/issues").format(param_name)
+ )