Move k8s modules to dynamic backend (#39632)

* Move k8s modules to dynamic backend
This commit is contained in:
Fabian von Feilitzsch 2018-05-16 11:57:36 -04:00 committed by Adam Miller
commit 4373b155a5
18 changed files with 540 additions and 1883 deletions

View file

@ -19,31 +19,17 @@
from __future__ import absolute_import, division, print_function
import os
import re
import copy
import json
from datetime import datetime
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
try:
from openshift.helper.kubernetes import KubernetesObjectHelper
from openshift.helper.openshift import OpenShiftObjectHelper
from openshift.helper.exceptions import KubernetesException
import kubernetes
from openshift.dynamic import DynamicClient
HAS_K8S_MODULE_HELPER = True
except ImportError as exc:
class KubernetesObjectHelper(object):
pass
class OpenShiftObjectHelper(object):
pass
except ImportError:
HAS_K8S_MODULE_HELPER = False
try:
@ -52,52 +38,188 @@ try:
except ImportError:
HAS_YAML = False
try:
import dictdiffer
HAS_DICTDIFFER = True
except ImportError:
HAS_DICTDIFFER = False
def remove_secret_data(obj_dict):
""" Remove any sensitive data from a K8s dict"""
if obj_dict.get('data'):
# Secret data
obj_dict.pop('data')
if obj_dict.get('string_data'):
# The API should not return sting_data in Secrets, but just in case
obj_dict.pop('string_data')
if obj_dict['metadata'].get('annotations'):
# Remove things like 'openshift.io/token-secret' from metadata
for key in [k for k in obj_dict['metadata']['annotations'] if 'secret' in k]:
obj_dict['metadata']['annotations'].pop(key)
def to_snake(name):
""" Convert a string from camel to snake """
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]'
return re.sub(p, _replace, name).lower()
class DateTimeEncoder(json.JSONEncoder):
# When using json.dumps() with K8s object, pass cls=DateTimeEncoder to handle any datetime objects
def default(self, o):
if isinstance(o, datetime):
return o.isoformat()
return json.JSONEncoder.default(self, o)
class KubernetesAnsibleModuleHelper(AnsibleMixin, KubernetesObjectHelper):
try:
import urllib3
urllib3.disable_warnings()
except ImportError:
pass
ARG_ATTRIBUTES_BLACKLIST = ('property_path',)
class KubernetesAnsibleModule(AnsibleModule):
COMMON_ARG_SPEC = {
'state': {
'default': 'present',
'choices': ['present', 'absent'],
},
'force': {
'type': 'bool',
'default': False,
},
'resource_definition': {
'type': 'dict',
'aliases': ['definition', 'inline']
},
'src': {
'type': 'path',
},
'kind': {},
'name': {},
'namespace': {},
'api_version': {
'default': 'v1',
'aliases': ['api', 'version'],
},
}
AUTH_ARG_SPEC = {
'kubeconfig': {
'type': 'path',
},
'context': {},
'host': {},
'api_key': {
'no_log': True,
},
'username': {},
'password': {
'no_log': True,
},
'verify_ssl': {
'type': 'bool',
},
'ssl_ca_cert': {
'type': 'path',
},
'cert_file': {
'type': 'path',
},
'key_file': {
'type': 'path',
},
}
class K8sAnsibleMixin(object):
_argspec_cache = None
@property
def argspec(self):
"""
Introspect the model properties, and return an Ansible module arg_spec dict.
:return: dict
"""
if self._argspec_cache:
return self._argspec_cache
argument_spec = copy.deepcopy(COMMON_ARG_SPEC)
argument_spec.update(copy.deepcopy(AUTH_ARG_SPEC))
self._argspec_cache = argument_spec
return self._argspec_cache
def get_api_client(self, **auth):
auth_args = AUTH_ARG_SPEC.keys()
auth = auth or getattr(self, 'params', {})
configuration = kubernetes.client.Configuration()
for key, value in iteritems(auth):
if key in auth_args and value is not None:
if key == 'api_key':
setattr(configuration, key, {'authorization': "Bearer {0}".format(value)})
else:
setattr(configuration, key, value)
elif key in auth_args and value is None:
env_value = os.getenv('K8S_AUTH_{0}'.format(key.upper()), None)
if env_value is not None:
setattr(configuration, key, env_value)
kubernetes.client.Configuration.set_default(configuration)
if auth.get('username') and auth.get('password') and auth.get('host'):
auth_method = 'params'
elif auth.get('api_key') and auth.get('host'):
auth_method = 'params'
elif auth.get('kubeconfig') or auth.get('context'):
auth_method = 'file'
else:
auth_method = 'default'
# First try to do incluster config, then kubeconfig
if auth_method == 'default':
try:
kubernetes.config.load_incluster_config()
return DynamicClient(kubernetes.client.ApiClient())
except kubernetes.config.ConfigException:
return DynamicClient(self.client_from_kubeconfig(auth.get('kubeconfig'), auth.get('context')))
if auth_method == 'file':
return DynamicClient(self.client_from_kubeconfig(auth.get('kubeconfig'), auth.get('context')))
if auth_method == 'params':
return DynamicClient(kubernetes.client.ApiClient(configuration))
def client_from_kubeconfig(self, config_file, context):
try:
return kubernetes.config.new_client_from_config(config_file, context)
except (IOError, kubernetes.config.ConfigException):
# If we failed to load the default config file then we'll return
# an empty configuration
# If one was specified, we will crash
if not config_file:
return kubernetes.client.ApiClient()
raise
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 load_resource_definitions(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:
with open(path, 'r') as f:
result = list(yaml.safe_load_all(f))
except (IOError, yaml.YAMLError) as exc:
self.fail_json(msg="Error loading resource_definition: {0}".format(exc))
return result
@staticmethod
def diff_objects(existing, new):
if not HAS_DICTDIFFER:
return False, []
def get_shared_attrs(o1, o2):
shared_attrs = {}
for k, v in o2.items():
if isinstance(v, dict):
shared_attrs[k] = get_shared_attrs(o1.get(k, {}), v)
else:
shared_attrs[k] = o1.get(k)
return shared_attrs
diffs = list(dictdiffer.diff(new, get_shared_attrs(existing, new)))
match = len(diffs) == 0
return match, diffs
class KubernetesAnsibleModule(AnsibleModule, K8sAnsibleMixin):
resource_definition = None
api_version = None
kind = None
helper = None
def __init__(self, *args, **kwargs):
@ -110,114 +232,5 @@ class KubernetesAnsibleModule(AnsibleModule):
if not HAS_YAML:
self.fail_json(msg="This module requires PyYAML. Try `pip install PyYAML`")
if not HAS_STRING_UTILS:
self.fail_json(msg="This module requires Python string utils. Try `pip install python-string-utils`")
@property
def argspec(self):
raise NotImplementedError()
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):
raise NotImplementedError()
def exit_json(self, **return_attributes):
""" Filter any sensitive data that we don't want logged """
if return_attributes.get('result') and \
return_attributes['result'].get('kind') in ('Secret', 'SecretList'):
if return_attributes['result'].get('data'):
remove_secret_data(return_attributes['result'])
elif return_attributes['result'].get('items'):
for item in return_attributes['result']['items']:
remove_secret_data(item)
super(KubernetesAnsibleModule, 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 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, [to_snake(key)], parameters)
return parameters
def _add_parameter(self, request, path, parameters):
for key, value in iteritems(request):
if path:
param_name = '_'.join(path + [to_snake(key)])
else:
param_name = 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(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))
)
class OpenShiftAnsibleModuleHelper(AnsibleMixin, OpenShiftObjectHelper):
pass
class OpenShiftAnsibleModuleMixin(object):
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.fail_json(msg="Error initializing module helper: {0}".format(exc.message))

View file

@ -1,633 +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 base64
import copy
from ansible.module_utils.six import iteritems, string_types
from keyword import kwlist
try:
from openshift.helper import PRIMITIVES
from openshift.helper.exceptions import KubernetesException
HAS_K8S_MODULE_HELPER = True
except ImportError as exc:
HAS_K8S_MODULE_HELPER = False
# TODO Remove string_utils dependency
try:
import string_utils
HAS_STRING_UTILS = True
except ImportError:
HAS_STRING_UTILS = False
ARG_ATTRIBUTES_BLACKLIST = ('property_path',)
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)]))
COMMON_ARG_SPEC = {
'state': {
'default': 'present',
'choices': ['present', 'absent'],
},
'force': {
'type': 'bool',
'default': False,
},
'resource_definition': {
'type': 'dict',
'aliases': ['definition', 'inline']
},
'src': {
'type': 'path',
},
'kind': {},
'name': {},
'namespace': {},
'api_version': {
'default': 'v1',
'aliases': ['api', 'version'],
},
}
AUTH_ARG_SPEC = {
'kubeconfig': {
'type': 'path',
},
'context': {},
'host': {},
'api_key': {
'no_log': True,
},
'username': {},
'password': {
'no_log': True,
},
'verify_ssl': {
'type': 'bool',
},
'ssl_ca_cert': {
'type': 'path',
},
'cert_file': {
'type': 'path',
},
'key_file': {
'type': 'path',
},
}
OPENSHIFT_ARG_SPEC = {
'description': {},
'display_name': {},
}
class AnsibleMixin(object):
_argspec_cache = None
@property
def argspec(self):
"""
Introspect the model properties, and return an Ansible module arg_spec dict.
:return: dict
"""
if self._argspec_cache:
return self._argspec_cache
argument_spec = copy.deepcopy(COMMON_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))
self._argspec_cache = argument_spec
return self._argspec_cache
def object_from_params(self, module_params, obj=None):
"""
Update a model object with Ansible module param values. Optionally pass an object
to update, otherwise a new object will be created.
:param module_params: dict of key:value pairs
:param obj: model object to update
:return: updated model object
"""
if not obj:
obj = self.model()
obj.kind = string_utils.snake_case_to_camel(self.kind, upper_case_first=False)
obj.api_version = self.api_version.lower()
for param_name, param_value in iteritems(module_params):
spec = self.find_arg_spec(param_name)
if param_value is not None and spec.get('property_path'):
prop_path = copy.copy(spec['property_path'])
self.__set_obj_attribute(obj, prop_path, param_value, param_name)
if self.kind.lower() == 'project' and (module_params.get('display_name') or
module_params.get('description')):
if not obj.metadata.annotations:
obj.metadata.annotations = {}
if module_params.get('display_name'):
obj.metadata.annotations['openshift.io/display-name'] = module_params['display_name']
if module_params.get('description'):
obj.metadata.annotations['openshift.io/description'] = module_params['description']
elif (self.kind.lower() == 'secret' and getattr(obj, 'string_data', None)
and hasattr(obj, 'data')):
if obj.data is None:
obj.data = {}
# Do a base64 conversion of `string_data` and place it in
# `data` so that later comparisons to existing objects
# (if any) do not result in requiring an unnecessary change.
for key, value in iteritems(obj.string_data):
obj.data[key] = base64.b64encode(value)
obj.string_data = None
return obj
def request_body_from_params(self, module_params):
request = {
'kind': self.base_model_name,
}
for param_name, param_value in iteritems(module_params):
spec = self.find_arg_spec(param_name)
if spec and spec.get('property_path') and param_value is not None:
self.__add_path_to_dict(request, param_name, param_value, spec['property_path'])
if self.kind.lower() == 'project' and (module_params.get('display_name') or
module_params.get('description')):
if not request.get('metadata'):
request['metadata'] = {}
if not request['metadata'].get('annotations'):
request['metadata']['annotations'] = {}
if module_params.get('display_name'):
request['metadata']['annotations']['openshift.io/display-name'] = module_params['display_name']
if module_params.get('description'):
request['metadata']['annotations']['openshift.io/description'] = module_params['description']
return request
def find_arg_spec(self, module_param_name):
"""For testing, allow the param_name value to be an alias"""
if module_param_name in self.argspec:
return self.argspec[module_param_name]
result = None
for key, value in iteritems(self.argspec):
if value.get('aliases'):
for alias in value['aliases']:
if alias == module_param_name:
result = self.argspec[key]
break
if result:
break
if not result:
raise KubernetesException(
"Error: received unrecognized module parameter {0}".format(module_param_name)
)
return result
@staticmethod
def __convert_params_to_choices(properties):
def snake_case(name):
result = string_utils.snake_case_to_camel(name.replace('_params', ''), upper_case_first=True)
return result[:1].upper() + result[1:]
choices = {}
for x in list(properties.keys()):
if x.endswith('params'):
choices[x] = snake_case(x)
return choices
def __add_path_to_dict(self, request_dict, param_name, param_value, path):
local_path = copy.copy(path)
spec = self.find_arg_spec(param_name)
while len(local_path):
p = string_utils.snake_case_to_camel(local_path.pop(0), upper_case_first=False)
if len(local_path):
if request_dict.get(p, None) is None:
request_dict[p] = {}
self.__add_path_to_dict(request_dict[p], param_name, param_value, local_path)
break
else:
param_type = spec.get('type', 'str')
if param_type == 'dict':
request_dict[p] = self.__dict_keys_to_camel(param_name, param_value)
elif param_type == 'list':
request_dict[p] = self.__list_keys_to_camel(param_name, param_value)
else:
request_dict[p] = param_value
def __dict_keys_to_camel(self, param_name, param_dict):
result = {}
for item, value in iteritems(param_dict):
key_name = self.__property_name_to_camel(param_name, item)
if value:
if isinstance(value, list):
result[key_name] = self.__list_keys_to_camel(param_name, value)
elif isinstance(value, dict):
result[key_name] = self.__dict_keys_to_camel(param_name, value)
else:
result[key_name] = value
return result
@staticmethod
def __property_name_to_camel(param_name, property_name):
new_name = property_name
if 'annotations' not in param_name and 'labels' not in param_name and 'selector' not in param_name:
camel_name = string_utils.snake_case_to_camel(property_name, upper_case_first=False)
new_name = camel_name[1:] if camel_name.startswith('_') else camel_name
return new_name
def __list_keys_to_camel(self, param_name, param_list):
result = []
if isinstance(param_list[0], dict):
for item in param_list:
result.append(self.__dict_keys_to_camel(param_name, item))
else:
result = param_list
return result
def __set_obj_attribute(self, obj, property_path, param_value, param_name):
"""
Recursively set object properties
:param obj: The object on which to set a property value.
:param property_path: A list of property names in the form of strings.
:param param_value: The value to set.
:return: The original object.
"""
while len(property_path) > 0:
raw_prop_name = property_path.pop(0)
prop_name = PYTHON_KEYWORD_MAPPING.get(raw_prop_name, raw_prop_name)
prop_kind = obj.swagger_types[prop_name]
if prop_kind in PRIMITIVES:
try:
setattr(obj, prop_name, param_value)
except ValueError as exc:
msg = str(exc)
if param_value is None and 'None' in msg:
pass
else:
raise KubernetesException(
"Error setting {0} to {1}: {2}".format(prop_name, param_value, msg)
)
elif prop_kind.startswith('dict('):
if not getattr(obj, prop_name):
setattr(obj, prop_name, param_value)
else:
self.__compare_dict(getattr(obj, prop_name), param_value, param_name)
elif prop_kind.startswith('list['):
if getattr(obj, prop_name) is None:
setattr(obj, prop_name, [])
obj_type = prop_kind.replace('list[', '').replace(']', '')
if obj_type not in PRIMITIVES and obj_type not in ('list', 'dict'):
self.__compare_obj_list(getattr(obj, prop_name), param_value, obj_type, param_name)
else:
self.__compare_list(getattr(obj, prop_name), param_value, param_name)
else:
# prop_kind is an object class
sub_obj = getattr(obj, prop_name)
if not sub_obj:
sub_obj = self.model_class_from_name(prop_kind)()
setattr(obj, prop_name, self.__set_obj_attribute(sub_obj, property_path, param_value, param_name))
return obj
def __compare_list(self, src_values, request_values, param_name):
"""
Compare src_values list with request_values list, and append any missing
request_values to src_values.
"""
if not request_values:
return
if not src_values:
src_values += request_values
if type(src_values[0]).__name__ in PRIMITIVES:
if set(src_values) >= set(request_values):
# src_value list includes request_value list
return
# append the missing elements from request value
src_values += list(set(request_values) - set(src_values))
elif type(src_values[0]).__name__ == 'dict':
missing = []
for request_dict in request_values:
match = False
for src_dict in src_values:
if '__cmp__' in dir(src_dict):
# python < 3
if src_dict >= request_dict:
match = True
break
elif iteritems(src_dict) == iteritems(request_dict):
# python >= 3
match = True
break
if not match:
missing.append(request_dict)
src_values += missing
elif type(src_values[0]).__name__ == 'list':
missing = []
for request_list in request_values:
match = False
for src_list in src_values:
if set(request_list) >= set(src_list):
match = True
break
if not match:
missing.append(request_list)
src_values += missing
else:
raise KubernetesException(
"Evaluating {0}: encountered unimplemented type {1} in "
"__compare_list()".format(param_name, type(src_values[0]).__name__)
)
def __compare_dict(self, src_value, request_value, param_name):
"""
Compare src_value dict with request_value dict, and update src_value with any differences.
Does not remove items from src_value dict.
"""
if not request_value:
return
for item, value in iteritems(request_value):
if type(value).__name__ in ('str', 'int', 'bool'):
src_value[item] = value
elif type(value).__name__ == 'list':
self.__compare_list(src_value[item], value, param_name)
elif type(value).__name__ == 'dict':
self.__compare_dict(src_value[item], value, param_name)
else:
raise KubernetesException(
"Evaluating {0}: encountered unimplemented type {1} in "
"__compare_dict()".format(param_name, type(value).__name__)
)
def __compare_obj_list(self, src_value, request_value, obj_class, param_name):
"""
Compare a src_value (list of ojects) with a request_value (list of dicts), and update
src_value with differences. Assumes each object and each dict has a 'name' attributes,
which can be used for matching. Elements are not removed from the src_value list.
"""
if not request_value:
return
model_class = self.model_class_from_name(obj_class)
# Try to determine the unique key for the array
key_names = [
'name',
'type'
]
key_name = None
for key in key_names:
if hasattr(model_class, key):
key_name = key
break
if key_name:
# If the key doesn't exist in the request values, then ignore it, rather than throwing an error
for item in request_value:
if not item.get(key_name):
key_name = None
break
if key_name:
# compare by key field
for item in request_value:
if not item.get(key_name):
# Prevent user from creating something that will be impossible to patch or update later
raise KubernetesException(
"Evaluating {0} - expecting parameter {1} to contain a `{2}` attribute "
"in __compare_obj_list().".format(param_name,
self.get_base_model_name_snake(obj_class),
key_name)
)
found = False
for obj in src_value:
if not obj:
continue
if getattr(obj, key_name) == item[key_name]:
# Assuming both the src_value and the request value include a name property
found = True
for key, value in iteritems(item):
snake_key = self.attribute_to_snake(key)
item_kind = model_class.swagger_types.get(snake_key)
if item_kind and item_kind in PRIMITIVES or type(value).__name__ in PRIMITIVES:
setattr(obj, snake_key, value)
elif item_kind and item_kind.startswith('list['):
obj_type = item_kind.replace('list[', '').replace(']', '')
if getattr(obj, snake_key) is None:
setattr(obj, snake_key, [])
if obj_type not in ('str', 'int', 'bool', 'object'):
self.__compare_obj_list(getattr(obj, snake_key), value, obj_type, param_name)
else:
# Straight list comparison
self.__compare_list(getattr(obj, snake_key), value, param_name)
elif item_kind and item_kind.startswith('dict('):
self.__compare_dict(getattr(obj, snake_key), value, param_name)
elif item_kind and type(value).__name__ == 'dict':
# object
param_obj = getattr(obj, snake_key)
if not param_obj:
setattr(obj, snake_key, self.model_class_from_name(item_kind)())
param_obj = getattr(obj, snake_key)
self.__update_object_properties(param_obj, value)
else:
if item_kind:
raise KubernetesException(
"Evaluating {0}: encountered unimplemented type {1} in "
"__compare_obj_list() for model {2}".format(
param_name,
item_kind,
self.get_base_model_name_snake(obj_class))
)
else:
raise KubernetesException(
"Evaluating {0}: unable to get swagger_type for {1} in "
"__compare_obj_list() for item {2} in model {3}".format(
param_name,
snake_key,
str(item),
self.get_base_model_name_snake(obj_class))
)
if not found:
# Requested item not found. Adding.
obj = self.model_class_from_name(obj_class)(**item)
src_value.append(obj)
else:
# There isn't a key, or we don't know what it is, so check for all properties to match
for item in request_value:
found = False
for obj in src_value:
match = True
for item_key, item_value in iteritems(item):
# TODO: this should probably take the property type into account
snake_key = self.attribute_to_snake(item_key)
if getattr(obj, snake_key) != item_value:
match = False
break
if match:
found = True
break
if not found:
obj = self.model_class_from_name(obj_class)(**item)
src_value.append(obj)
def __update_object_properties(self, obj, item):
""" Recursively update an object's properties. Returns a pointer to the object. """
for key, value in iteritems(item):
snake_key = self.attribute_to_snake(key)
try:
kind = obj.swagger_types[snake_key]
except (AttributeError, KeyError):
possible_matches = ', '.join(list(obj.swagger_types.keys()))
class_snake_name = self.get_base_model_name_snake(type(obj).__name__)
raise KubernetesException(
"Unable to find '{0}' in {1}. Valid property names include: {2}".format(snake_key,
class_snake_name,
possible_matches)
)
if kind in PRIMITIVES or kind.startswith('list[') or kind.startswith('dict('):
self.__set_obj_attribute(obj, [snake_key], value, snake_key)
else:
# kind is an object, hopefully
if not getattr(obj, snake_key):
setattr(obj, snake_key, self.model_class_from_name(kind)())
self.__update_object_properties(getattr(obj, snake_key), value)
return obj
def __transform_properties(self, properties, prefix='', path=None, alternate_prefix=''):
"""
Convert a list of properties to an argument_spec dictionary
:param properties: List of properties from self.properties_from_model_class()
:param prefix: String to prefix to argument names.
:param path: List of property names providing the recursive path through the model to the property
:param alternate_prefix: a more minimal version of prefix
:return: dict
"""
primitive_types = list(PRIMITIVES) + ['list', 'dict']
args = {}
if path is None:
path = []
def add_meta(prop_name, prop_prefix, prop_alt_prefix):
""" Adds metadata properties to the argspec """
# if prop_alt_prefix != prop_prefix:
# if prop_alt_prefix:
# args[prop_prefix + prop_name]['aliases'] = [prop_alt_prefix + prop_name]
# elif prop_prefix:
# args[prop_prefix + prop_name]['aliases'] = [prop_name]
prop_paths = copy.copy(path) # copy path from outer scope
prop_paths.append('metadata')
prop_paths.append(prop_name)
args[prop_prefix + prop_name]['property_path'] = prop_paths
for raw_prop, prop_attributes in iteritems(properties):
prop = PYTHON_KEYWORD_MAPPING.get(raw_prop, raw_prop)
if prop in ('api_version', 'status', 'kind', 'items') and not prefix:
# Don't expose these properties
continue
elif prop_attributes['immutable']:
# Property cannot be set by the user
continue
elif prop == 'metadata' and prop_attributes['class'].__name__ == 'UnversionedListMeta':
args['namespace'] = {}
elif prop == 'metadata' and prop_attributes['class'].__name__ != 'UnversionedListMeta':
meta_prefix = prefix + '_metadata_' if prefix else ''
meta_alt_prefix = alternate_prefix + '_metadata_' if alternate_prefix else ''
if meta_prefix and not meta_alt_prefix:
meta_alt_prefix = meta_prefix
if 'labels' in dir(prop_attributes['class']):
args[meta_prefix + 'labels'] = {
'type': 'dict',
}
add_meta('labels', meta_prefix, meta_alt_prefix)
if 'annotations' in dir(prop_attributes['class']):
args[meta_prefix + 'annotations'] = {
'type': 'dict',
}
add_meta('annotations', meta_prefix, meta_alt_prefix)
if 'namespace' in dir(prop_attributes['class']):
args[meta_prefix + 'namespace'] = {}
add_meta('namespace', meta_prefix, meta_alt_prefix)
if 'name' in dir(prop_attributes['class']):
args[meta_prefix + 'name'] = {}
add_meta('name', meta_prefix, meta_alt_prefix)
elif prop_attributes['class'].__name__ not in primitive_types and not prop.endswith('params'):
# Adds nested properties recursively
label = prop
# Provide a more human-friendly version of the prefix
alternate_label = label\
.replace('spec', '')\
.replace('template', '')\
.replace('config', '')
p = prefix
p += '_' + label if p else label
a = alternate_prefix
paths = copy.copy(path)
paths.append(prop)
# if alternate_prefix:
# # Prevent the last prefix from repeating. In other words, avoid things like 'pod_pod'
# pieces = alternate_prefix.split('_')
# alternate_label = alternate_label.replace(pieces[len(pieces) - 1] + '_', '', 1)
# if alternate_label != self.base_model_name and alternate_label not in a:
# a += '_' + alternate_label if a else alternate_label
if prop.endswith('params') and 'type' in properties:
sub_props = dict()
sub_props[prop] = {
'class': dict,
'immutable': False
}
args.update(self.__transform_properties(sub_props, prefix=p, path=paths, alternate_prefix=a))
else:
sub_props = self.properties_from_model_class(prop_attributes['class'])
args.update(self.__transform_properties(sub_props, prefix=p, path=paths, alternate_prefix=a))
else:
# Adds a primitive property
arg_prefix = prefix + '_' if prefix else ''
arg_alt_prefix = alternate_prefix + '_' if alternate_prefix else ''
paths = copy.copy(path)
paths.append(prop)
property_type = prop_attributes['class'].__name__
if property_type == 'object':
property_type = 'str'
args[arg_prefix + prop] = {
'required': False,
'type': property_type,
'property_path': paths
}
if prop.endswith('params') and 'type' in properties:
args[arg_prefix + prop]['type'] = 'dict'
# Use the alternate prefix to construct a human-friendly alias
if arg_alt_prefix and arg_prefix != arg_alt_prefix:
args[arg_prefix + prop]['aliases'] = [arg_alt_prefix + prop]
elif arg_prefix:
args[arg_prefix + prop]['aliases'] = [prop]
if prop == 'type':
choices = self.__convert_params_to_choices(properties)
if len(choices) > 0:
args[arg_prefix + prop]['choices'] = choices
return args

View file

@ -18,22 +18,24 @@
from __future__ import absolute_import, division, print_function
from ansible.module_utils.six import iteritems
from ansible.module_utils.k8s.common import K8sAnsibleMixin, HAS_K8S_MODULE_HELPER
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:
HAS_K8S_MODULE_HELPER = False
from ansible.errors import AnsibleError
except ImportError:
AnsibleError = Exception
try:
from openshift.dynamic.exceptions import DynamicApiError
except ImportError:
pass
class K8sInventoryException(Exception):
pass
class K8sInventoryHelper(object):
class K8sInventoryHelper(K8sAnsibleMixin):
helper = None
transport = 'kubectl'
@ -56,7 +58,7 @@ class K8sInventoryHelper(object):
self.fetch_objects(connections)
def fetch_objects(self, connections):
self.helper = self.get_helper('v1', 'namespace_list')
client = self.get_api_client()
if connections:
if not isinstance(connections, list):
@ -65,68 +67,50 @@ class K8sInventoryHelper(object):
for connection in connections:
if not isinstance(connection, dict):
raise K8sInventoryException("Expecting connection to be a dictionary.")
self.authenticate(connection)
name = connection.get('name', self.get_default_host_name(self.helper.api_client.host))
client = self.get_api_client(**connection)
name = connection.get('name', self.get_default_host_name(client.configuration.host))
if connection.get('namespaces'):
namespaces = connections['namespaces']
else:
namespaces = self.get_available_namespaces()
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_pods_for_namespace(name, namespace)
self.get_services_for_namespace(name, namespace)
self.get_pods_for_namespace(client, name, namespace)
self.get_services_for_namespace(client, name, namespace)
else:
name = self.get_default_host_name(self.helper.api_client.host)
namespaces = self.get_available_namespaces()
name = self.get_default_host_name(client.configuration.host)
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_pods_for_namespace(name, namespace)
self.get_services_for_namespace(name, namespace)
def authenticate(self, connection=None):
auth_options = {}
if connection:
auth_args = ('host', 'api_key', 'kubeconfig', 'context', 'username', 'password',
'cert_file', 'key_file', 'ssl_ca_cert', 'verify_ssl')
for key, value in iteritems(connection):
if key in auth_args and value is not None:
auth_options[key] = value
try:
self.helper.set_client_config(**auth_options)
except KubernetesException as exc:
raise K8sInventoryException('Error connecting to the API: {0}'.format(exc.message))
self.get_pods_for_namespace(client, name, namespace)
self.get_services_for_namespace(client, name, namespace)
@staticmethod
def get_default_host_name(host):
return host.replace('https://', '').replace('http://', '').replace('.', '-').replace(':', '_')
def get_helper(self, api_version, kind):
def get_available_namespaces(self, client):
v1_namespace = client.resources.get(api_version='v1', kind='Namespace')
try:
helper = KubernetesObjectHelper(api_version=api_version, kind=kind, debug=False)
helper.get_model(api_version, kind)
return helper
except KubernetesException as exc:
raise K8sInventoryException('Error initializing object helper: {0}'.format(exc.message))
def get_available_namespaces(self):
try:
obj = self.helper.get_object()
except KubernetesObjectHelper as exc:
obj = v1_namespace.get()
except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Namespace list: {0}'.format(exc.message))
return [namespace.metadata.name for namespace in obj.items]
def get_pods_for_namespace(self, name, namespace):
self.helper.set_model('v1', 'pod_list')
def get_pods_for_namespace(self, client, name, namespace):
v1_pod = client.resources.get(api_version='v1', kind='Pod')
try:
obj = self.helper.get_object(namespace=namespace)
except KubernetesException as exc:
obj = v1_pod.get(namespace=namespace)
except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Pod list: {0}'.format(exc.message))
namespace_pod_group = '{0}_pods'.format(namespace)
namespace_group = 'namespace_{0}'.format(namespace)
namespace_pods_group = '{0}_pods'.format(namespace_group)
self.inventory.add_group(name)
self.inventory.add_group(namespace)
self.inventory.add_child(name, namespace)
self.inventory.add_group(namespace_pod_group)
self.inventory.add_child(namespace, namespace_pod_group)
self.inventory.add_group(namespace_group)
self.inventory.add_child(name, namespace_group)
self.inventory.add_group(namespace_pods_group)
self.inventory.add_child(namespace_group, namespace_pods_group)
for pod in obj.items:
pod_name = pod.metadata.name
pod_groups = []
@ -136,17 +120,17 @@ class K8sInventoryHelper(object):
if pod.metadata.labels:
pod_labels = pod.metadata.labels
# create a group for each label_value
for key, value in iteritems(pod.metadata.labels):
group_name = '{0}_{1}'.format(key, value)
for key, value in pod.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value)
if group_name not in pod_groups:
pod_groups.append(group_name)
self.inventory.add_group(group_name)
for container in pod.status.container_statuses:
for container in pod.status.containerStatuses:
# add each pod_container to the namespace group, and to each label_value group
container_name = '{0}_{1}'.format(pod.metadata.name, container.name)
self.inventory.add_host(container_name)
self.inventory.add_child(namespace_pod_group, container_name)
self.inventory.add_child(namespace_pods_group, container_name)
if pod_groups:
for group in pod_groups:
self.inventory.add_child(group, container_name)
@ -155,14 +139,14 @@ class K8sInventoryHelper(object):
self.inventory.set_variable(container_name, 'object_type', 'pod')
self.inventory.set_variable(container_name, 'labels', pod_labels)
self.inventory.set_variable(container_name, 'annotations', pod_annotations)
self.inventory.set_variable(container_name, 'cluster_name', pod.metadata.cluster_name)
self.inventory.set_variable(container_name, 'pod_node_name', pod.spec.node_name)
self.inventory.set_variable(container_name, 'pod_name', pod.spec.node_name)
self.inventory.set_variable(container_name, 'pod_host_ip', pod.status.host_ip)
self.inventory.set_variable(container_name, 'cluster_name', pod.metadata.clusterName)
self.inventory.set_variable(container_name, 'pod_node_name', pod.spec.nodeName)
self.inventory.set_variable(container_name, 'pod_name', pod.spec.name)
self.inventory.set_variable(container_name, 'pod_host_ip', pod.status.hostIP)
self.inventory.set_variable(container_name, 'pod_phase', pod.status.phase)
self.inventory.set_variable(container_name, 'pod_ip', pod.status.pod_ip)
self.inventory.set_variable(container_name, 'pod_self_link', pod.metadata.self_link)
self.inventory.set_variable(container_name, 'pod_resource_version', pod.metadata.resource_version)
self.inventory.set_variable(container_name, 'pod_ip', pod.status.podIP)
self.inventory.set_variable(container_name, 'pod_self_link', pod.metadata.selfLink)
self.inventory.set_variable(container_name, 'pod_resource_version', pod.metadata.resourceVersion)
self.inventory.set_variable(container_name, 'pod_uid', pod.metadata.uid)
self.inventory.set_variable(container_name, 'container_name', container.image)
self.inventory.set_variable(container_name, 'container_image', container.image)
@ -179,20 +163,22 @@ class K8sInventoryHelper(object):
self.inventory.set_variable(container_name, 'ansible_{0}_container'.format(self.transport),
container.name)
def get_services_for_namespace(self, name, namespace):
self.helper.set_model('v1', 'service_list')
def get_services_for_namespace(self, client, name, namespace):
v1_service = client.resources.get(api_version='v1', kind='Service')
try:
obj = self.helper.get_object(namespace=namespace)
except KubernetesException as exc:
obj = v1_service.get(namespace=namespace)
except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Service list: {0}'.format(exc.message))
namespace_service_group = '{0}_services'.format(namespace)
namespace_group = 'namespace_{0}'.format(namespace)
namespace_services_group = '{0}_services'.format(namespace_group)
self.inventory.add_group(name)
self.inventory.add_group(namespace)
self.inventory.add_child(name, namespace)
self.inventory.add_group(namespace_service_group)
self.inventory.add_child(namespace, namespace_service_group)
self.inventory.add_group(namespace_group)
self.inventory.add_child(name, namespace_group)
self.inventory.add_group(namespace_services_group)
self.inventory.add_child(namespace_group, namespace_services_group)
for service in obj.items:
service_name = service.metadata.name
service_labels = {} if not service.metadata.labels else service.metadata.labels
@ -202,51 +188,54 @@ class K8sInventoryHelper(object):
if service.metadata.labels:
# create a group for each label_value
for key, value in iteritems(service.metadata.labels):
group_name = '{0}_{1}'.format(key, value)
for key, value in service.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value)
self.inventory.add_group(group_name)
self.inventory.add_child(group_name, service_name)
self.inventory.add_child(namespace_service_group, service_name)
try:
self.inventory.add_child(namespace_services_group, service_name)
except AnsibleError as e:
raise
ports = [{'name': port.name,
'port': port.port,
'protocol': port.protocol,
'targetPort': port.target_port,
'nodePort': port.node_port} for port in service.spec.ports]
'targetPort': port.targetPort,
'nodePort': port.nodePort} for port in service.spec.ports or []]
# add hostvars
self.inventory.set_variable(service_name, 'object_type', 'service')
self.inventory.set_variable(service_name, 'labels', service_labels)
self.inventory.set_variable(service_name, 'annotations', service_annotations)
self.inventory.set_variable(service_name, 'cluster_name', service.metadata.cluster_name)
self.inventory.set_variable(service_name, 'cluster_name', service.metadata.clusterName)
self.inventory.set_variable(service_name, 'ports', ports)
self.inventory.set_variable(service_name, 'type', service.spec.type)
self.inventory.set_variable(service_name, 'self_link', service.metadata.self_link)
self.inventory.set_variable(service_name, 'resource_version', service.metadata.resource_version)
self.inventory.set_variable(service_name, 'self_link', service.metadata.selfLink)
self.inventory.set_variable(service_name, 'resource_version', service.metadata.resourceVersion)
self.inventory.set_variable(service_name, 'uid', service.metadata.uid)
if service.spec.external_traffic_policy:
if service.spec.externalTrafficPolicy:
self.inventory.set_variable(service_name, 'external_traffic_policy',
service.spec.external_traffic_policy)
if hasattr(service.spec, 'external_ips') and service.spec.external_ips:
self.inventory.set_variable(service_name, 'external_ips', service.spec.external_ips)
service.spec.externalTrafficPolicy)
if service.spec.externalIPs:
self.inventory.set_variable(service_name, 'external_ips', service.spec.externalIPs)
if service.spec.external_name:
self.inventory.set_variable(service_name, 'external_name', service.spec.external_name)
if service.spec.externalName:
self.inventory.set_variable(service_name, 'external_name', service.spec.externalName)
if service.spec.health_check_node_port:
if service.spec.healthCheckNodePort:
self.inventory.set_variable(service_name, 'health_check_node_port',
service.spec.health_check_node_port)
if service.spec.load_balancer_ip:
service.spec.healthCheckNodePort)
if service.spec.loadBalancerIP:
self.inventory.set_variable(service_name, 'load_balancer_ip',
service.spec.load_balancer_ip)
service.spec.loadBalancerIP)
if service.spec.selector:
self.inventory.set_variable(service_name, 'selector', service.spec.selector)
if hasattr(service.status.load_balancer, 'ingress') and service.status.load_balancer.ingress:
if hasattr(service.status.loadBalancer, 'ingress') and service.status.loadBalancer.ingress:
load_balancer = [{'hostname': ingress.hostname,
'ip': ingress.ip} for ingress in service.status.load_balancer.ingress]
'ip': ingress.ip} for ingress in service.status.loadBalancer.ingress]
self.inventory.set_variable(service_name, 'load_balancer', load_balancer)
@ -256,46 +245,39 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
def fetch_objects(self, connections):
super(OpenShiftInventoryHelper, self).fetch_objects(connections)
self.helper = self.get_helper('v1', 'namespace_list')
client = self.get_api_client()
if connections:
for connection in connections:
self.authenticate(connection)
name = connection.get('name', self.get_default_host_name(self.helper.api_client.host))
client = self.get_api_client(**connection)
name = connection.get('name', self.get_default_host_name(client.configuration.host))
if connection.get('namespaces'):
namespaces = connection['namespaces']
else:
namespaces = self.get_available_namespaces()
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_routes_for_namespace(name, namespace)
self.get_routes_for_namespace(client, name, namespace)
else:
name = self.get_default_host_name(self.helper.api_client.host)
namespaces = self.get_available_namespaces()
name = self.get_default_host_name(client.configuration.host)
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_routes_for_namespace(name, namespace)
self.get_routes_for_namespace(client, name, namespace)
def get_helper(self, api_version, kind):
def get_routes_for_namespace(self, client, name, namespace):
v1_route = client.resources.get(api_version='v1', kind='Route')
try:
helper = OpenShiftObjectHelper(api_version=api_version, kind=kind, debug=False)
helper.get_model(api_version, kind)
return helper
except KubernetesException as exc:
raise K8sInventoryException('Error initializing object helper: {0}'.format(exc.message))
def get_routes_for_namespace(self, name, namespace):
self.helper.set_model('v1', 'route_list')
try:
obj = self.helper.get_object(namespace=namespace)
except KubernetesException as exc:
obj = v1_route.get(namespace=namespace)
except DynamicApiError as exc:
raise K8sInventoryException('Error fetching Routes list: {0}'.format(exc.message))
namespace_routes_group = '{0}_routes'.format(namespace)
namespace_group = 'namespace_{0}'.format(namespace)
namespace_routes_group = '{0}_routes'.format(namespace_group)
self.inventory.add_group(name)
self.inventory.add_group(namespace)
self.inventory.add_child(name, namespace)
self.inventory.add_group(namespace_group)
self.inventory.add_child(name, namespace_group)
self.inventory.add_group(namespace_routes_group)
self.inventory.add_child(namespace, namespace_routes_group)
self.inventory.add_child(namespace_group, namespace_routes_group)
for route in obj.items:
route_name = route.metadata.name
route_labels = {} if not route.metadata.labels else route.metadata.labels
@ -305,8 +287,8 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
if route.metadata.labels:
# create a group for each label_value
for key, value in iteritems(route.metadata.labels):
group_name = '{0}_{1}'.format(key, value)
for key, value in route.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value)
self.inventory.add_group(group_name)
self.inventory.add_child(group_name, route_name)
@ -315,10 +297,10 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
# add hostvars
self.inventory.set_variable(route_name, 'labels', route_labels)
self.inventory.set_variable(route_name, 'annotations', route_annotations)
self.inventory.set_variable(route_name, 'cluster_name', route.metadata.cluster_name)
self.inventory.set_variable(route_name, 'cluster_name', route.metadata.clusterName)
self.inventory.set_variable(route_name, 'object_type', 'route')
self.inventory.set_variable(route_name, 'self_link', route.metadata.self_link)
self.inventory.set_variable(route_name, 'resource_version', route.metadata.resource_version)
self.inventory.set_variable(route_name, 'self_link', route.metadata.selfLink)
self.inventory.set_variable(route_name, 'resource_version', route.metadata.resourceVersion)
self.inventory.set_variable(route_name, 'uid', route.metadata.uid)
if route.spec.host:
@ -327,5 +309,5 @@ class OpenShiftInventoryHelper(K8sInventoryHelper):
if route.spec.path:
self.inventory.set_variable(route_name, 'path', route.spec.path)
if hasattr(route.spec.port, 'target_port') and route.spec.port.target_port:
if hasattr(route.spec.port, 'targetPort') and route.spec.port.targetPort:
self.inventory.set_variable(route_name, 'port', route.spec.port)

View file

@ -1,208 +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/>.
from __future__ import absolute_import, division, print_function
import json
import os
from ansible.module_utils.k8s.common import OpenShiftAnsibleModuleMixin, DateTimeEncoder, remove_secret_data, to_snake
from ansible.module_utils.k8s.helper import AUTH_ARG_SPEC
try:
from openshift.helper.kubernetes import KubernetesObjectHelper
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 KubernetesLookup(object):
def __init__(self):
if not HAS_K8S_MODULE_HELPER:
raise Exception(
"Requires the OpenShift Python client. Try `pip install openshift`"
)
if not HAS_YAML:
raise Exception(
"Requires PyYAML. Try `pip install PyYAML`"
)
self.kind = None
self.name = None
self.namespace = None
self.api_version = None
self.label_selector = None
self.field_selector = None
self.include_uninitialized = None
self.resource_definition = None
self.helper = None
self.connection = {}
def run(self, terms, variables=None, **kwargs):
self.kind = kwargs.get('kind')
self.name = kwargs.get('resource_name')
self.namespace = kwargs.get('namespace')
self.api_version = kwargs.get('api_version', 'v1')
self.label_selector = kwargs.get('label_selector')
self.field_selector = kwargs.get('field_selector')
self.include_uninitialized = kwargs.get('include_uninitialized', False)
resource_definition = kwargs.get('resource_definition')
src = kwargs.get('src')
if src:
resource_definition = self.load_resource_definition(src)
if resource_definition:
self.params_from_resource_definition(resource_definition)
if not self.kind:
raise Exception(
"Error: no Kind specified. Use the 'kind' parameter, or provide an object YAML configuration "
"using the 'resource_definition' parameter."
)
self.kind = to_snake(self.kind)
self.helper = self.get_helper(self.api_version, self.kind)
auth_args = ('host', 'api_key', 'kubeconfig', 'context', 'username', 'password',
'cert_file', 'key_file', 'ssl_ca_cert', 'verify_ssl')
for arg in AUTH_ARG_SPEC:
if arg in auth_args and kwargs.get(arg) is not None:
self.connection[arg] = kwargs.get(arg)
try:
self.helper.set_client_config(**self.connection)
except Exception as exc:
raise Exception(
"Client authentication failed: {0}".format(exc.message)
)
if self.name:
return self.get_object()
return self.list_objects()
def get_helper(self, api_version, kind):
try:
helper = KubernetesObjectHelper(api_version=api_version, kind=kind, debug=False)
helper.get_model(api_version, kind)
return helper
except KubernetesException as exc:
raise Exception("Error initializing helper: {0}".format(exc.message))
def load_resource_definition(self, src):
""" Load the requested src path """
path = os.path.normpath(src)
if not os.path.exists(path):
raise Exception("Error accessing {0}. Does the file exist?".format(path))
try:
result = yaml.safe_load(open(path, 'r'))
except (IOError, yaml.YAMLError) as exc:
raise Exception("Error loading resource_definition: {0}".format(exc))
return result
def params_from_resource_definition(self, defn):
if defn.get('apiVersion'):
self.api_version = defn['apiVersion']
if defn.get('kind'):
self.kind = defn['kind']
if defn.get('metadata', {}).get('name'):
self.name = defn['metadata']['name']
if defn.get('metadata', {}).get('namespace'):
self.namespace = defn['metadata']['namespace']
def get_object(self):
""" Fetch a named object """
try:
result = self.helper.get_object(self.name, self.namespace)
except KubernetesException as exc:
raise Exception('Failed to retrieve requested object: {0}'.format(exc.message))
response = []
if result is not None:
# Convert Datetime objects to ISO format
result_json = json.loads(json.dumps(result.to_dict(), cls=DateTimeEncoder))
if self.kind == 'secret':
remove_secret_data(result_json)
response.append(result_json)
return response
def list_objects(self):
""" Query for a set of objects """
if self.namespace:
method_name = 'list_namespaced_{0}'.format(self.kind)
try:
method = self.helper.lookup_method(method_name=method_name)
except KubernetesException:
raise Exception(
"Failed to find method {0} for API {1}".format(method_name, self.api_version)
)
else:
method_name = 'list_{0}_for_all_namespaces'.format(self.kind)
try:
method = self.helper.lookup_method(method_name=method_name)
except KubernetesException:
method_name = 'list_{0}'.format(self.kind)
try:
method = self.helper.lookup_method(method_name=method_name)
except KubernetesException:
raise Exception(
"Failed to find method for API {0} and Kind {1}".format(self.api_version, self.kind)
)
params = {}
if self.field_selector:
params['field_selector'] = self.field_selector
if self.label_selector:
params['label_selector'] = self.label_selector
params['include_uninitialized'] = self.include_uninitialized
if self.namespace:
try:
result = method(self.namespace, **params)
except KubernetesException as exc:
raise Exception(exc.message)
else:
try:
result = method(**params)
except KubernetesException as exc:
raise Exception(exc.message)
response = []
if result is not None:
# Convert Datetime objects to ISO format
result_json = json.loads(json.dumps(result.to_dict(), cls=DateTimeEncoder))
response = result_json.get('items', [])
if self.kind == 'secret':
for item in response:
remove_secret_data(item)
return response
class OpenShiftLookup(OpenShiftAnsibleModuleMixin, KubernetesLookup):
pass

View file

@ -18,13 +18,12 @@
from __future__ import absolute_import, division, print_function
import copy
from ansible.module_utils.k8s.helper import COMMON_ARG_SPEC, AUTH_ARG_SPEC, OPENSHIFT_ARG_SPEC
from ansible.module_utils.k8s.common import KubernetesAnsibleModule, OpenShiftAnsibleModuleMixin, to_snake
from ansible.module_utils.k8s.common import KubernetesAnsibleModule
try:
from openshift.helper.exceptions import KubernetesException
from openshift.dynamic.exceptions import DynamicApiError, NotFoundError, ConflictError
except ImportError:
# Exception handled in common
pass
@ -33,6 +32,8 @@ except ImportError:
class KubernetesRawModule(KubernetesAnsibleModule):
def __init__(self, *args, **kwargs):
self.client = None
mutually_exclusive = [
('resource_definition', 'src'),
]
@ -42,170 +43,140 @@ class KubernetesRawModule(KubernetesAnsibleModule):
supports_check_mode=True,
**kwargs)
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)
kind = self.params.pop('kind')
api_version = self.params.pop('api_version')
name = self.params.pop('name')
namespace = self.params.pop('namespace')
resource_definition = self.params.pop('resource_definition')
if resource_definition:
self.resource_definitions = [resource_definition]
src = self.params.pop('src')
if src:
self.resource_definitions = self.load_resource_definitions(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 = 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
if not resource_definition and not src:
self.resource_definitions = [{
'kind': kind,
'apiVersion': api_version,
'metadata': {
'name': name,
'namespace': namespace
}
}]
def execute_module(self):
if self.resource_definition:
resource_params = self.resource_to_parameters(self.resource_definition)
self.params.update(resource_params)
changed = False
results = []
self.client = self.get_api_client()
for definition in self.resource_definitions:
kind = definition.get('kind')
search_kind = kind
if kind.lower().endswith('list'):
search_kind = kind[:-4]
api_version = definition.get('apiVersion')
try:
resource = self.client.resources.get(kind=search_kind, api_version=api_version)
except Exception as e:
self.fail_json(msg='Failed to find resource {0}.{1}: {2}'.format(
api_version, search_kind, e
))
result = self.perform_action(resource, definition)
changed = changed or result['changed']
results.append(result)
self.authenticate()
if len(results) == 1:
self.exit_json(**results[0])
self.exit_json(**{
'changed': changed,
'result': {
'results': results
}
})
def perform_action(self, resource, definition):
result = {'changed': False, 'result': {}}
state = self.params.pop('state', None)
force = self.params.pop('force', False)
name = self.params.get('name')
namespace = self.params.get('namespace')
name = definition.get('metadata', {}).get('name')
namespace = definition.get('metadata', {}).get('namespace')
existing = None
self.remove_aliases()
return_attributes = dict(changed=False, result=dict())
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)
if definition['kind'].endswith('list'):
result['result'] = resource.get(namespace=namespace).to_dict()
result['changed'] = False
result['method'] = 'get'
return result
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'))
existing = resource.get(name=name, namespace=namespace)
except NotFoundError:
pass
except DynamicApiError as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
if state == 'absent':
result['method'] = "delete"
if not existing:
# The object already does not exist
self.exit_json(**return_attributes)
return result
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)
k8s_obj = resource.delete(name, namespace=namespace)
result['result'] = k8s_obj.to_dict()
except DynamicApiError as exc:
self.fail_json(msg="Failed to delete object: {0}".format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
result['changed'] = True
return result
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)
k8s_obj = resource.create(definition, namespace=namespace)
except ConflictError:
# Some resources, like ProjectRequests, can't be created multiple times,
# because the resources that they create don't match their kind
# In this case we'll mark it as unchanged and warn the user
self.warn("{0} was not found, but creating it returned a 409 Conflict error. This can happen \
if the resource you are creating does not directly create a resource of the same kind.".format(name))
return result
result['result'] = k8s_obj.to_dict()
result['changed'] = True
result['method'] = 'create'
return result
if existing and force:
if not self.check_mode:
try:
k8s_obj = resource.replace(definition, name=name, namespace=namespace)
result['result'] = k8s_obj.to_dict()
except DynamicApiError as exc:
self.fail_json(msg="Failed to replace object: {0}".format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
result['changed'] = True
result['method'] = 'replace'
return result
match, diffs = self.diff_objects(existing.to_dict(), definition)
# 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(self.helper.fix_serialization(existing), k8s_obj)
if match:
return_attributes['result'] = existing.to_dict()
self.exit_json(**return_attributes)
result['result'] = existing.to_dict()
return result
# 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 _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
class OpenShiftRawModule(OpenShiftAnsibleModuleMixin, KubernetesRawModule):
@property
def argspec(self):
args = super(OpenShiftRawModule, self).argspec
args.update(copy.deepcopy(OPENSHIFT_ARG_SPEC))
return args
def _create(self, namespace):
if self.kind.lower() == 'project':
return self._create_project()
return KubernetesRawModule._create(self, 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: {0}".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
k8s_obj = resource.patch(definition, name=name, namespace=namespace)
result['result'] = k8s_obj.to_dict()
except DynamicApiError as exc:
self.fail_json(msg="Failed to patch object: {0}".format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
result['changed'] = True
result['method'] = 'patch'
result['diff'] = diffs
return result

View file

@ -22,13 +22,12 @@ import copy
import math
import time
from ansible.module_utils.six import iteritems
from ansible.module_utils.k8s.common import OpenShiftAnsibleModuleMixin
from ansible.module_utils.k8s.raw import KubernetesRawModule
from ansible.module_utils.k8s.helper import AUTH_ARG_SPEC, COMMON_ARG_SPEC
from ansible.module_utils.k8s.common import AUTH_ARG_SPEC, COMMON_ARG_SPEC
try:
from openshift import watch
from openshift.dynamic.client import ResourceInstance
from openshift.helper.exceptions import KubernetesException
except ImportError as exc:
class KubernetesException(Exception):
@ -47,14 +46,12 @@ SCALE_ARG_SPEC = {
class KubernetesAnsibleScaleModule(KubernetesRawModule):
def execute_module(self):
if self.resource_definition:
resource_params = self.resource_to_parameters(self.resource_definition)
self.params.update(resource_params)
definition = self.resource_definitions[0]
self.authenticate()
self.client = self.get_api_client()
name = self.params.get('name')
namespace = self.params.get('namespace')
name = definition['metadata']['name']
namespace = definition['metadata'].get('namespace')
current_replicas = self.params.get('current_replicas')
replicas = self.params.get('replicas')
resource_version = self.params.get('resource_version')
@ -65,8 +62,10 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
existing_count = None
return_attributes = dict(changed=False, result=dict())
resource = self.client.resources.get(api_version=definition['apiVersion'], kind=definition['kind'])
try:
existing = self.helper.get_object(name, namespace)
existing = resource.get(name=name, namespace=namespace)
return_attributes['result'] = existing.to_dict()
except KubernetesException as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.message),
@ -80,7 +79,7 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
if existing_count is None:
self.fail_json(msg='Failed to retrieve the available count for the requested object.')
if resource_version and resource_version != existing.metadata.resource_version:
if resource_version and resource_version != existing.metadata.resourceVersion:
self.exit_json(**return_attributes)
if current_replicas is not None and existing_count != current_replicas:
@ -91,25 +90,13 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
if not self.check_mode:
if self.kind == 'job':
existing.spec.parallelism = replicas
k8s_obj = self.helper.patch_object(name, namespace, existing)
k8s_obj = resource.patch(existing.to_dict())
else:
k8s_obj = self.scale(existing, replicas, wait, wait_time)
k8s_obj = self.scale(resource, existing, replicas, wait, wait_time)
return_attributes['result'] = k8s_obj.to_dict()
self.exit_json(**return_attributes)
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', 'resourceVersion'):
parameters[meta_key] = meta_value
return parameters
@property
def argspec(self):
args = copy.deepcopy(COMMON_ARG_SPEC)
@ -119,91 +106,67 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
args.update(SCALE_ARG_SPEC)
return args
def scale(self, existing_object, replicas, wait, wait_time):
def scale(self, resource, existing_object, replicas, wait, wait_time):
name = existing_object.metadata.name
namespace = existing_object.metadata.namespace
method_name = 'patch_namespaced_{0}_scale'.format(self.kind)
method = None
model = None
try:
method = self.helper.lookup_method(method_name=method_name)
except KubernetesException:
if not hasattr(resource, 'scale'):
self.fail_json(
msg="Failed to get method {0}. Is 'scale' a valid operation for {1}?".format(method_name, self.kind)
msg="Cannot perform scale on resource of kind {0}".format(resource.kind)
)
try:
model = self.helper.get_model(self.api_version, 'scale')
except KubernetesException:
self.fail_json(
msg="Failed to fetch the 'Scale' model for API version {0}. Are you using the correct "
"API?".format(self.api_version)
)
scale_obj = model()
scale_obj.kind = 'scale'
scale_obj.api_version = self.api_version.lower()
scale_obj.metadata = self.helper.get_model(
self.api_version,
self.helper.get_base_model_name(scale_obj.swagger_types['metadata'])
)()
scale_obj.metadata.name = name
scale_obj.metadata.namespace = namespace
scale_obj.spec = self.helper.get_model(
self.api_version,
self.helper.get_base_model_name(scale_obj.swagger_types['spec'])
)()
scale_obj.spec.replicas = replicas
scale_obj = {'metadata': {'name': name, 'namespace': namespace}, 'spec': {'replicas': replicas}}
return_obj = None
stream = None
if wait:
w, stream = self._create_stream(namespace, wait_time)
w, stream = self._create_stream(resource, namespace, wait_time)
try:
method(name, namespace, scale_obj)
resource.scale.patch(body=scale_obj)
except Exception as exc:
self.fail_json(
msg="Scale request failed: {0}".format(exc.message)
)
if wait and stream is not None:
return_obj = self._read_stream(w, stream, name, replicas)
return_obj = self._read_stream(resource, w, stream, name, replicas)
if not return_obj:
return_obj = self._wait_for_response(name, namespace)
return return_obj
def _create_stream(self, namespace, wait_time):
def _create_stream(self, resource, namespace, wait_time):
""" Create a stream of events for the object """
w = None
stream = None
try:
list_method = self.helper.lookup_method('list', namespace)
w = watch.Watch()
w._api_client = self.helper.api_client
w._api_client = self.client.client
if namespace:
stream = w.stream(list_method, namespace, timeout_seconds=wait_time)
stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
else:
stream = w.stream(list_method, timeout_seconds=wait_time)
stream = w.stream(resource.get, serialize=False, namespace=namespace, timeout_seconds=wait_time)
except KubernetesException:
pass
except Exception:
raise
return w, stream
def _read_stream(self, watcher, stream, name, replicas):
def _read_stream(self, resource, watcher, stream, name, replicas):
""" Wait for ready_replicas to equal the requested number of replicas. """
return_obj = None
try:
for event in stream:
if event.get('object'):
obj = event['object']
obj = ResourceInstance(resource, event['object'])
if obj.metadata.name == name and hasattr(obj, 'status'):
if hasattr(obj.status, 'ready_replicas') and obj.status.ready_replicas == replicas:
if replicas == 0:
if not hasattr(obj.status, 'readyReplicas') or not obj.status.readyReplicas:
return_obj = obj
watcher.stop()
break
if hasattr(obj.status, 'readyReplicas') and obj.status.readyReplicas == replicas:
return_obj = obj
watcher.stop()
break
@ -212,27 +175,23 @@ class KubernetesAnsibleScaleModule(KubernetesRawModule):
if not return_obj:
self.fail_json(msg="Error fetching the patched object. Try a higher wait_timeout value.")
if return_obj.status.ready_replicas is None:
if replicas and return_obj.status.readyReplicas is None:
self.fail_json(msg="Failed to fetch the number of ready replicas. Try a higher wait_timeout value.")
if return_obj.status.ready_replicas != replicas:
if replicas and return_obj.status.readyReplicas != replicas:
self.fail_json(msg="Number of ready replicas is {0}. Failed to reach {1} ready replicas within "
"the wait_timeout period.".format(return_obj.status.ready_replicas, replicas))
return return_obj
def _wait_for_response(self, name, namespace):
def _wait_for_response(self, resource, name, namespace):
""" Wait for an API response """
tries = 0
half = math.ceil(20 / 2)
obj = None
while tries <= half:
obj = self.helper.get_object(name, namespace)
obj = resource.get(name=name, namespace=namespace)
if obj:
break
tries += 2
time.sleep(2)
return obj
class OpenShiftAnsibleScaleModule(OpenShiftAnsibleModuleMixin, KubernetesAnsibleScaleModule):
pass