Reorganize k8s common (#34212)
* Reorganize k8s common * Separates AnsibleMixin * Fix lint issues
This commit is contained in:
parent
8ddb84a155
commit
9b4a1ed475
6 changed files with 386 additions and 375 deletions
0
lib/ansible/module_utils/k8s/__init__.py
Normal file
0
lib/ansible/module_utils/k8s/__init__.py
Normal file
368
lib/ansible/module_utils/k8s/common.py
Normal file
368
lib/ansible/module_utils/k8s/common.py
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
#
|
||||||
|
# Copyright 2018 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/>.
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from ansible.module_utils.six import iteritems
|
||||||
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
|
||||||
|
from ansible.module_utils.k8s.helper import\
|
||||||
|
AnsibleMixin,\
|
||||||
|
HAS_STRING_UTILS,\
|
||||||
|
COMMON_ARG_SPEC,\
|
||||||
|
AUTH_ARG_SPEC,\
|
||||||
|
OPENSHIFT_ARG_SPEC
|
||||||
|
|
||||||
|
try:
|
||||||
|
from openshift.helper.kubernetes import KubernetesObjectHelper
|
||||||
|
from openshift.helper.openshift import OpenShiftObjectHelper
|
||||||
|
from openshift.helper.exceptions import KubernetesException
|
||||||
|
HAS_K8S_MODULE_HELPER = True
|
||||||
|
except ImportError as exc:
|
||||||
|
class KubernetesObjectHelper(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class OpenShiftObjectHelper(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
HAS_K8S_MODULE_HELPER = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
HAS_YAML = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_YAML = False
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesAnsibleModuleHelper(AnsibleMixin, KubernetesObjectHelper):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesAnsibleModule(AnsibleModule):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
if not HAS_K8S_MODULE_HELPER:
|
||||||
|
raise Exception(
|
||||||
|
"This module requires the OpenShift Python client. Try `pip install openshift`"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_YAML:
|
||||||
|
raise Exception(
|
||||||
|
"This module requires PyYAML. Try `pip install PyYAML`"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_STRING_UTILS:
|
||||||
|
raise Exception(
|
||||||
|
"This module requires Python string utils. Try `pip install python-string-utils`"
|
||||||
|
)
|
||||||
|
|
||||||
|
mutually_exclusive = [
|
||||||
|
('resource_definition', 'src'),
|
||||||
|
]
|
||||||
|
|
||||||
|
AnsibleModule.__init__(self,
|
||||||
|
argument_spec=self._argspec,
|
||||||
|
supports_check_mode=True,
|
||||||
|
mutually_exclusive=mutually_exclusive)
|
||||||
|
|
||||||
|
self.kind = self.params.pop('kind')
|
||||||
|
self.api_version = self.params.pop('api_version')
|
||||||
|
self.resource_definition = self.params.pop('resource_definition')
|
||||||
|
self.src = self.params.pop('src')
|
||||||
|
if self.src:
|
||||||
|
self.resource_definition = self.load_resource_definition(self.src)
|
||||||
|
|
||||||
|
if self.resource_definition:
|
||||||
|
self.api_version = self.resource_definition.get('apiVersion')
|
||||||
|
self.kind = self.resource_definition.get('kind')
|
||||||
|
|
||||||
|
self.api_version = self.api_version.lower()
|
||||||
|
self.kind = self._to_snake(self.kind)
|
||||||
|
|
||||||
|
if not self.api_version:
|
||||||
|
self.fail_json(
|
||||||
|
msg=("Error: no api_version specified. Use the api_version parameter, or provide it as part of a ",
|
||||||
|
"resource_definition.")
|
||||||
|
)
|
||||||
|
if not self.kind:
|
||||||
|
self.fail_json(
|
||||||
|
msg="Error: no kind specified. Use the kind parameter, or provide it as part of a resource_definition"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper = self._get_helper(self.api_version, self.kind)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _argspec(self):
|
||||||
|
argspec = copy.deepcopy(COMMON_ARG_SPEC)
|
||||||
|
argspec.update(copy.deepcopy(AUTH_ARG_SPEC))
|
||||||
|
return argspec
|
||||||
|
|
||||||
|
def _get_helper(self, api_version, kind):
|
||||||
|
try:
|
||||||
|
helper = KubernetesAnsibleModuleHelper(api_version=api_version, kind=kind, debug=False)
|
||||||
|
helper.get_model(api_version, kind)
|
||||||
|
return helper
|
||||||
|
except KubernetesException as exc:
|
||||||
|
self.fail_json(msg="Error initializing module helper {0}".format(exc.message))
|
||||||
|
|
||||||
|
def execute_module(self):
|
||||||
|
if self.resource_definition:
|
||||||
|
resource_params = self.resource_to_parameters(self.resource_definition)
|
||||||
|
self.params.update(resource_params)
|
||||||
|
|
||||||
|
self._authenticate()
|
||||||
|
|
||||||
|
state = self.params.pop('state', None)
|
||||||
|
force = self.params.pop('force', False)
|
||||||
|
name = self.params.get('name')
|
||||||
|
namespace = self.params.get('namespace')
|
||||||
|
existing = None
|
||||||
|
|
||||||
|
self._remove_aliases()
|
||||||
|
|
||||||
|
return_attributes = dict(changed=False, result=dict())
|
||||||
|
|
||||||
|
if self._diff:
|
||||||
|
return_attributes['request'] = self.helper.request_body_from_params(self.params)
|
||||||
|
|
||||||
|
if self.helper.base_model_name_snake.endswith('list'):
|
||||||
|
k8s_obj = self._read(name, namespace)
|
||||||
|
return_attributes['result'] = k8s_obj.to_dict()
|
||||||
|
self.exit_json(**return_attributes)
|
||||||
|
|
||||||
|
try:
|
||||||
|
existing = self.helper.get_object(name, namespace)
|
||||||
|
except KubernetesException as exc:
|
||||||
|
self.fail_json(msg='Failed to retrieve requested object: {0}'.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: {0}".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['result'] = 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: {0}".format(exc.message),
|
||||||
|
error=exc.value.get('status'))
|
||||||
|
return_attributes['result'] = 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: {0}".format(exc.message))
|
||||||
|
match, diff = self.helper.objects_match(existing, k8s_obj)
|
||||||
|
if match:
|
||||||
|
return_attributes['result'] = existing.to_dict()
|
||||||
|
self.exit_json(**return_attributes)
|
||||||
|
elif self._diff:
|
||||||
|
return_attributes['differences'] = diff
|
||||||
|
# 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: {0}".format(exc.message))
|
||||||
|
return_attributes['result'] = k8s_obj.to_dict()
|
||||||
|
return_attributes['changed'] = True
|
||||||
|
self.exit_json(**return_attributes)
|
||||||
|
|
||||||
|
def _authenticate(self):
|
||||||
|
try:
|
||||||
|
auth_options = {}
|
||||||
|
auth_args = ('host', 'api_key', 'kubeconfig', 'context', 'username', 'password',
|
||||||
|
'cert_file', 'key_file', 'ssl_ca_cert', 'verify_ssl')
|
||||||
|
for key, value in iteritems(self.params):
|
||||||
|
if key in auth_args and value is not None:
|
||||||
|
auth_options[key] = value
|
||||||
|
self.helper.set_client_config(**auth_options)
|
||||||
|
except KubernetesException as e:
|
||||||
|
self.fail_json(msg='Error loading config', error=str(e))
|
||||||
|
|
||||||
|
def _remove_aliases(self):
|
||||||
|
"""
|
||||||
|
The helper doesn't know what to do with aliased keys
|
||||||
|
"""
|
||||||
|
for k, v in iteritems(self._argspec):
|
||||||
|
if 'aliases' in v:
|
||||||
|
for alias in v['aliases']:
|
||||||
|
if alias in self.params:
|
||||||
|
self.params.pop(alias)
|
||||||
|
|
||||||
|
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: {0}".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: {0}".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)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
self.fail_json(msg="Error accessing {0}. 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: {0}".format(exc))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def resource_to_parameters(self, resource):
|
||||||
|
""" Converts a resource definition to module parameters """
|
||||||
|
parameters = {}
|
||||||
|
for key, value in iteritems(resource):
|
||||||
|
if key in ('apiVersion', 'kind', 'status'):
|
||||||
|
continue
|
||||||
|
elif key == 'metadata' and isinstance(value, dict):
|
||||||
|
for meta_key, meta_value in iteritems(value):
|
||||||
|
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)
|
||||||
|
return parameters
|
||||||
|
|
||||||
|
def _add_parameter(self, request, path, parameters):
|
||||||
|
for key, value in iteritems(request):
|
||||||
|
if path:
|
||||||
|
param_name = '_'.join(path + [self._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 {0}, which does not map to a parameter "
|
||||||
|
"expected by the OpenShift Python module.".format(param_name))
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _to_snake(name):
|
||||||
|
"""
|
||||||
|
Convert a string from camel to snake
|
||||||
|
:param name: string to convert
|
||||||
|
:return: string
|
||||||
|
"""
|
||||||
|
if not name:
|
||||||
|
return name
|
||||||
|
|
||||||
|
def replace(m):
|
||||||
|
m = m.group(0)
|
||||||
|
return m[0] + '_' + m[1:]
|
||||||
|
|
||||||
|
p = r'[a-z][A-Z]|' \
|
||||||
|
r'[A-Z]{2}[a-z]'
|
||||||
|
result = re.sub(p, replace, name)
|
||||||
|
return result.lower()
|
||||||
|
|
||||||
|
|
||||||
|
class OpenShiftAnsibleModuleHelper(AnsibleMixin, OpenShiftObjectHelper):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OpenShiftAnsibleModule(KubernetesAnsibleModule):
|
||||||
|
def __init__(self):
|
||||||
|
super(OpenShiftAnsibleModule, self).__init__()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _argspec(self):
|
||||||
|
args = super(OpenShiftAnsibleModule, self)._argspec
|
||||||
|
args.update(copy.deepcopy(OPENSHIFT_ARG_SPEC))
|
||||||
|
return args
|
||||||
|
|
||||||
|
def _get_helper(self, api_version, kind):
|
||||||
|
try:
|
||||||
|
helper = OpenShiftAnsibleModuleHelper(api_version=api_version, kind=kind, debug=False)
|
||||||
|
helper.get_model(api_version, kind)
|
||||||
|
return helper
|
||||||
|
except KubernetesException as exc:
|
||||||
|
self.exit_json(msg="Error initializing module helper {}".format(exc.message))
|
||||||
|
|
||||||
|
def _create(self, namespace):
|
||||||
|
if self.kind.lower() == 'project':
|
||||||
|
return self._create_project()
|
||||||
|
return super(OpenShiftAnsibleModule, self)._create(namespace)
|
||||||
|
|
||||||
|
def _create_project(self):
|
||||||
|
new_obj = None
|
||||||
|
k8s_obj = None
|
||||||
|
try:
|
||||||
|
new_obj = self.helper.object_from_params(self.params)
|
||||||
|
except KubernetesException as exc:
|
||||||
|
self.fail_json(msg="Failed to create object: {}".format(exc.message))
|
||||||
|
try:
|
||||||
|
k8s_obj = self.helper.create_project(metadata=new_obj.metadata,
|
||||||
|
display_name=self.params.get('display_name'),
|
||||||
|
description=self.params.get('description'))
|
||||||
|
except KubernetesException as exc:
|
||||||
|
self.fail_json(msg='Failed to retrieve requested object',
|
||||||
|
error=exc.value.get('status'))
|
||||||
|
return k8s_obj
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright 2017 Red Hat | Ansible
|
# Copyright 2018 Red Hat | Ansible
|
||||||
#
|
#
|
||||||
# This file is part of Ansible
|
# This file is part of Ansible
|
||||||
#
|
#
|
||||||
|
@ -16,34 +16,20 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import absolute_import, division, print_function
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import copy
|
|
||||||
import base64
|
import base64
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from ansible.module_utils.six import iteritems, string_types
|
||||||
from keyword import kwlist
|
from keyword import kwlist
|
||||||
|
|
||||||
from ansible.module_utils.six import iteritems
|
|
||||||
from ansible.module_utils.basic import AnsibleModule
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from openshift.helper import PRIMITIVES
|
from openshift.helper import PRIMITIVES
|
||||||
from openshift.helper.kubernetes import KubernetesObjectHelper
|
|
||||||
from openshift.helper.exceptions import KubernetesException
|
from openshift.helper.exceptions import KubernetesException
|
||||||
HAS_K8S_MODULE_HELPER = True
|
HAS_K8S_MODULE_HELPER = True
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
class KubernetesObjectHelper(object):
|
|
||||||
pass
|
|
||||||
HAS_K8S_MODULE_HELPER = False
|
HAS_K8S_MODULE_HELPER = False
|
||||||
|
|
||||||
try:
|
# TODO Remove string_utils dependency
|
||||||
import yaml
|
|
||||||
HAS_YAML = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_YAML = False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import string_utils
|
import string_utils
|
||||||
HAS_STRING_UTILS = True
|
HAS_STRING_UTILS = True
|
||||||
|
@ -55,7 +41,7 @@ ARG_ATTRIBUTES_BLACKLIST = ('property_path',)
|
||||||
PYTHON_KEYWORD_MAPPING = dict(zip(['_{0}'.format(item) for item in kwlist], kwlist))
|
PYTHON_KEYWORD_MAPPING = dict(zip(['_{0}'.format(item) for item in kwlist], kwlist))
|
||||||
PYTHON_KEYWORD_MAPPING.update(dict([reversed(item) for item in iteritems(PYTHON_KEYWORD_MAPPING)]))
|
PYTHON_KEYWORD_MAPPING.update(dict([reversed(item) for item in iteritems(PYTHON_KEYWORD_MAPPING)]))
|
||||||
|
|
||||||
ARG_SPEC = {
|
COMMON_ARG_SPEC = {
|
||||||
'state': {
|
'state': {
|
||||||
'default': 'present',
|
'default': 'present',
|
||||||
'choices': ['present', 'absent'],
|
'choices': ['present', 'absent'],
|
||||||
|
@ -74,12 +60,13 @@ ARG_SPEC = {
|
||||||
'kind': {},
|
'kind': {},
|
||||||
'name': {},
|
'name': {},
|
||||||
'namespace': {},
|
'namespace': {},
|
||||||
'description': {},
|
|
||||||
'display_name': {},
|
|
||||||
'api_version': {
|
'api_version': {
|
||||||
'default': 'v1',
|
'default': 'v1',
|
||||||
'aliases': ['api', 'version'],
|
'aliases': ['api', 'version'],
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
AUTH_ARG_SPEC = {
|
||||||
'kubeconfig': {
|
'kubeconfig': {
|
||||||
'type': 'path',
|
'type': 'path',
|
||||||
},
|
},
|
||||||
|
@ -106,6 +93,11 @@ ARG_SPEC = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OPENSHIFT_ARG_SPEC = {
|
||||||
|
'description': {},
|
||||||
|
'display_name': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AnsibleMixin(object):
|
class AnsibleMixin(object):
|
||||||
_argspec_cache = None
|
_argspec_cache = None
|
||||||
|
@ -118,8 +110,9 @@ class AnsibleMixin(object):
|
||||||
"""
|
"""
|
||||||
if self._argspec_cache:
|
if self._argspec_cache:
|
||||||
return self._argspec_cache
|
return self._argspec_cache
|
||||||
|
argument_spec = copy.deepcopy(COMMON_ARG_SPEC)
|
||||||
argument_spec = copy.deepcopy(ARG_SPEC)
|
argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC))
|
||||||
|
argument_spec.update(copy.deepcopy(OPENSHIFT_ARG_SPEC))
|
||||||
argument_spec.update(self.__transform_properties(self.properties))
|
argument_spec.update(self.__transform_properties(self.properties))
|
||||||
self._argspec_cache = argument_spec
|
self._argspec_cache = argument_spec
|
||||||
return self._argspec_cache
|
return self._argspec_cache
|
||||||
|
@ -638,275 +631,3 @@ class AnsibleMixin(object):
|
||||||
if len(choices) > 0:
|
if len(choices) > 0:
|
||||||
args[arg_prefix + prop]['choices'] = choices
|
args[arg_prefix + prop]['choices'] = choices
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
class KubernetesAnsibleModuleHelper(AnsibleMixin, KubernetesObjectHelper):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class KubernetesAnsibleModule(AnsibleModule):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
if not HAS_K8S_MODULE_HELPER:
|
|
||||||
raise Exception(
|
|
||||||
"This module requires the OpenShift Python client. Try `pip install openshift`"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not HAS_YAML:
|
|
||||||
raise Exception(
|
|
||||||
"This module requires PyYAML. Try `pip install PyYAML`"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not HAS_STRING_UTILS:
|
|
||||||
raise Exception(
|
|
||||||
"This module requires Python string utils. Try `pip install python-string-utils`"
|
|
||||||
)
|
|
||||||
|
|
||||||
mutually_exclusive = [
|
|
||||||
('resource_definition', 'src'),
|
|
||||||
]
|
|
||||||
|
|
||||||
AnsibleModule.__init__(self,
|
|
||||||
argument_spec=self._argspec,
|
|
||||||
supports_check_mode=True,
|
|
||||||
mutually_exclusive=mutually_exclusive)
|
|
||||||
|
|
||||||
self.kind = self.params.pop('kind')
|
|
||||||
self.api_version = self.params.pop('api_version')
|
|
||||||
self.resource_definition = self.params.pop('resource_definition')
|
|
||||||
self.src = self.params.pop('src')
|
|
||||||
if self.src:
|
|
||||||
self.resource_definition = self.load_resource_definition(self.src)
|
|
||||||
|
|
||||||
if self.resource_definition:
|
|
||||||
self.api_version = self.resource_definition.get('apiVersion')
|
|
||||||
self.kind = self.resource_definition.get('kind')
|
|
||||||
|
|
||||||
self.api_version = self.api_version.lower()
|
|
||||||
self.kind = self._to_snake(self.kind)
|
|
||||||
|
|
||||||
if not self.api_version:
|
|
||||||
self.fail_json(
|
|
||||||
msg=("Error: no api_version specified. Use the api_version parameter, or provide it as part of a ",
|
|
||||||
"resource_definition.")
|
|
||||||
)
|
|
||||||
if not self.kind:
|
|
||||||
self.fail_json(
|
|
||||||
msg="Error: no kind specified. Use the kind parameter, or provide it as part of a resource_definition"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.helper = self._get_helper(self.api_version, self.kind)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _argspec(self):
|
|
||||||
argspec = copy.deepcopy(ARG_SPEC)
|
|
||||||
argspec.pop('display_name')
|
|
||||||
argspec.pop('description')
|
|
||||||
return argspec
|
|
||||||
|
|
||||||
def _get_helper(self, api_version, kind):
|
|
||||||
try:
|
|
||||||
helper = KubernetesAnsibleModuleHelper(api_version=api_version, kind=kind, debug=False)
|
|
||||||
helper.get_model(api_version, kind)
|
|
||||||
return helper
|
|
||||||
except KubernetesException as exc:
|
|
||||||
self.fail_json(msg="Error initializing module helper {0}".format(exc.message))
|
|
||||||
|
|
||||||
def execute_module(self):
|
|
||||||
if self.resource_definition:
|
|
||||||
resource_params = self.resource_to_parameters(self.resource_definition)
|
|
||||||
self.params.update(resource_params)
|
|
||||||
|
|
||||||
self._authenticate()
|
|
||||||
|
|
||||||
state = self.params.pop('state', None)
|
|
||||||
force = self.params.pop('force', False)
|
|
||||||
name = self.params.get('name')
|
|
||||||
namespace = self.params.get('namespace')
|
|
||||||
existing = None
|
|
||||||
|
|
||||||
self._remove_aliases()
|
|
||||||
|
|
||||||
return_attributes = dict(changed=False, result=dict())
|
|
||||||
|
|
||||||
if self._diff:
|
|
||||||
return_attributes['request'] = self.helper.request_body_from_params(self.params)
|
|
||||||
|
|
||||||
if self.helper.base_model_name_snake.endswith('list'):
|
|
||||||
k8s_obj = self._read(name, namespace)
|
|
||||||
return_attributes['result'] = k8s_obj.to_dict()
|
|
||||||
self.exit_json(**return_attributes)
|
|
||||||
|
|
||||||
try:
|
|
||||||
existing = self.helper.get_object(name, namespace)
|
|
||||||
except KubernetesException as exc:
|
|
||||||
self.fail_json(msg='Failed to retrieve requested object: {0}'.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: {0}".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['result'] = 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: {0}".format(exc.message),
|
|
||||||
error=exc.value.get('status'))
|
|
||||||
return_attributes['result'] = 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: {0}".format(exc.message))
|
|
||||||
match, diff = self.helper.objects_match(existing, k8s_obj)
|
|
||||||
if match:
|
|
||||||
return_attributes['result'] = existing.to_dict()
|
|
||||||
self.exit_json(**return_attributes)
|
|
||||||
elif self._diff:
|
|
||||||
return_attributes['differences'] = diff
|
|
||||||
# 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: {0}".format(exc.message))
|
|
||||||
return_attributes['result'] = k8s_obj.to_dict()
|
|
||||||
return_attributes['changed'] = True
|
|
||||||
self.exit_json(**return_attributes)
|
|
||||||
|
|
||||||
def _authenticate(self):
|
|
||||||
try:
|
|
||||||
auth_options = {}
|
|
||||||
auth_args = ('host', 'api_key', 'kubeconfig', 'context', 'username', 'password',
|
|
||||||
'cert_file', 'key_file', 'ssl_ca_cert', 'verify_ssl')
|
|
||||||
for key, value in iteritems(self.params):
|
|
||||||
if key in auth_args and value is not None:
|
|
||||||
auth_options[key] = value
|
|
||||||
self.helper.set_client_config(**auth_options)
|
|
||||||
except KubernetesException as e:
|
|
||||||
self.fail_json(msg='Error loading config', error=str(e))
|
|
||||||
|
|
||||||
def _remove_aliases(self):
|
|
||||||
"""
|
|
||||||
The helper doesn't know what to do with aliased keys
|
|
||||||
"""
|
|
||||||
for k, v in iteritems(self._argspec):
|
|
||||||
if 'aliases' in v:
|
|
||||||
for alias in v['aliases']:
|
|
||||||
if alias in self.params:
|
|
||||||
self.params.pop(alias)
|
|
||||||
|
|
||||||
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: {0}".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: {0}".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)
|
|
||||||
if not os.path.exists(path):
|
|
||||||
self.fail_json(msg="Error accessing {0}. 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: {0}".format(exc))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def resource_to_parameters(self, resource):
|
|
||||||
""" Converts a resource definition to module parameters """
|
|
||||||
parameters = {}
|
|
||||||
for key, value in iteritems(resource):
|
|
||||||
if key in ('apiVersion', 'kind', 'status'):
|
|
||||||
continue
|
|
||||||
elif key == 'metadata' and isinstance(value, dict):
|
|
||||||
for meta_key, meta_value in iteritems(value):
|
|
||||||
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)
|
|
||||||
return parameters
|
|
||||||
|
|
||||||
def _add_parameter(self, request, path, parameters):
|
|
||||||
for key, value in iteritems(request):
|
|
||||||
if path:
|
|
||||||
param_name = '_'.join(path + [self._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 {0}, which does not map to a parameter "
|
|
||||||
"expected by the OpenShift Python module.".format(param_name))
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _to_snake(name):
|
|
||||||
"""
|
|
||||||
Convert a string from camel to snake
|
|
||||||
:param name: string to convert
|
|
||||||
:return: string
|
|
||||||
"""
|
|
||||||
if not name:
|
|
||||||
return name
|
|
||||||
|
|
||||||
def replace(m):
|
|
||||||
m = m.group(0)
|
|
||||||
return m[0] + '_' + m[1:]
|
|
||||||
|
|
||||||
p = r'[a-z][A-Z]|' \
|
|
||||||
r'[A-Z]{2}[a-z]'
|
|
||||||
result = re.sub(p, replace, name)
|
|
||||||
return result.lower()
|
|
|
@ -1,78 +0,0 @@
|
||||||
#
|
|
||||||
# Copyright 2018 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
|
|
||||||
|
|
||||||
from ansible.module_utils.k8s_common import KubernetesAnsibleModule, AnsibleMixin, ARG_SPEC
|
|
||||||
|
|
||||||
try:
|
|
||||||
from openshift.helper.openshift import OpenShiftObjectHelper
|
|
||||||
from openshift.helper.exceptions import KubernetesException
|
|
||||||
HAS_OPENSHIFT_HELPER = True
|
|
||||||
except ImportError as exc:
|
|
||||||
class OpenShiftObjectHelper(object):
|
|
||||||
pass
|
|
||||||
HAS_OPENSHIFT_HELPER = False
|
|
||||||
|
|
||||||
|
|
||||||
class OpenShiftAnsibleModuleHelper(AnsibleMixin, OpenShiftObjectHelper):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OpenShiftAnsibleModule(KubernetesAnsibleModule):
|
|
||||||
def __init__(self):
|
|
||||||
|
|
||||||
if not HAS_OPENSHIFT_HELPER:
|
|
||||||
raise Exception(
|
|
||||||
"This module requires the OpenShift Python client. Try `pip install openshift`"
|
|
||||||
)
|
|
||||||
|
|
||||||
super(OpenShiftAnsibleModule, self).__init__()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _argspec(self):
|
|
||||||
return copy.deepcopy(ARG_SPEC)
|
|
||||||
|
|
||||||
def _get_helper(self, api_version, kind):
|
|
||||||
try:
|
|
||||||
helper = OpenShiftAnsibleModuleHelper(api_version=api_version, kind=kind, debug=False)
|
|
||||||
helper.get_model(api_version, kind)
|
|
||||||
return helper
|
|
||||||
except KubernetesException as exc:
|
|
||||||
self.exit_json(msg="Error initializing module helper {}".format(exc.message))
|
|
||||||
|
|
||||||
def _create(self, namespace):
|
|
||||||
if self.kind.lower() == 'project':
|
|
||||||
return self._create_project()
|
|
||||||
return super(OpenShiftAnsibleModule, self)._create(namespace)
|
|
||||||
|
|
||||||
def _create_project(self):
|
|
||||||
new_obj = None
|
|
||||||
k8s_obj = None
|
|
||||||
try:
|
|
||||||
new_obj = self.helper.object_from_params(self.params)
|
|
||||||
except KubernetesException as exc:
|
|
||||||
self.fail_json(msg="Failed to create object: {}".format(exc.message))
|
|
||||||
try:
|
|
||||||
k8s_obj = self.helper.create_project(metadata=new_obj.metadata,
|
|
||||||
display_name=self.params.get('display_name'),
|
|
||||||
description=self.params.get('description'))
|
|
||||||
except KubernetesException as exc:
|
|
||||||
self.fail_json(msg='Failed to retrieve requested object',
|
|
||||||
error=exc.value.get('status'))
|
|
||||||
return k8s_obj
|
|
|
@ -136,7 +136,7 @@ diff:
|
||||||
type: list
|
type: list
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from ansible.module_utils.k8s_common import KubernetesAnsibleModule
|
from ansible.module_utils.k8s.common import KubernetesAnsibleModule
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
@ -176,7 +176,7 @@ diff:
|
||||||
type: list
|
type: list
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from ansible.module_utils.openshift_common import OpenShiftAnsibleModule
|
from ansible.module_utils.k8s.common import OpenShiftAnsibleModule
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
Loading…
Add table
Reference in a new issue