1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/lib/ansible/module_utils/k8s_common.py
2017-03-30 19:02:17 +02:00

297 lines
12 KiB
Python

#
# 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 <http://www.gnu.org/licenses/>.
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 = {}
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
<kind>: 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)
name = self.params.get('name')
namespace = self.params.get('namespace', None)
existing = None
return_attributes = dict(changed=False, api_version=self.api_version)
return_attributes[self.helper.base_model_name_snake] = {}
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)
)